- 추상 멤버 : 클래스나 트레이트의 멤버가 그 클래스 안에 완전한 정의를 갖지 않는 것, 상속한 서브 클래스에서 구현해야 한다.

- val, var, 메소드, 타입 추상 멤버 존재

20.1 추상 멤버 간략하게 돌아보기

trait Abstract {
  type T
  def transform(x: T): T
  val initial: T
  var current: T
}

 

class Concrete extends Abstract {
  type T = String
  def transform(x: String) = x + x
  val initial = "hi"
  var current = initial
}

 

20.2 타입 멤버

: 스칼라의 추상 타입은 클래스나 트레이트의 멤버로 정의 없이 선언된 타입(클래스나 트레이트 자체는 추상 타입 X)

: 구체적(비 추상) 타입 멤버는 어떤 타입에 대한 새로운 이름 또는 별명

: 타입 멤버 => 실제 이름이 너무 길거나 의미가 불명확할 때 더 간단하고 의도를 잘 전달할 수 있다. => 코드가 명확 

: 서브 클래스에서 타입이 정해지는 경우 사용

 

20.3 추상 val

: val에 대해 이름과 타입은 주지만 값 지정 X

: 클래스 안에서 어떤 변수에 대해 정확한 값을 알 수 없지만, 그 변수가 클래스의 인스턴스에서 변하지 않으리란 사실을 알고 있을 때 추상 val 사용

: 추상 val 선언은 추상 메소드 선언과 비슷해 보인다.

val initial: String

def initial: String

=> 클라이언트 코드는 obj.initial로 val와 메소드를 같은 방식으로 사용 가능

but, val이라면 같은 값을 얻을 수 있다는 확신이 있지만 메소드라면 그런 보장이 없다.

=> 추상 val은 구현시 val 정의를 사용해야한다는 제약

=> 추상 메소드 선언은 val이나 메소드 정의로 사용 가능하다.

 

abstract class Fruit {
  val v: String // `v' for value
  def m: String // `m' for method
}

abstract class Apple extends Fruit {
  val v: String
  val m: String // OK to override a `def' with a `val'
}

abstract class BadApple extends Fruit {
  def v: String // ERROR: cannot override a `val' with a `def'
  def m: String
}

 

20.4 추상 var

: 추상 var도 암시적으로 게터 메소드와 세터 메소드를 정의하는 것과 같다.

trait AbstractTime {
  var hour: Int
  var minute: Int
}
trait AbstractTime {
  def hour: Int          // getter for `hour'
  def hour_=(x: Int)     // setter for `hour'
  def minute: Int        // getter for `minute'
  def minute_=(x: Int)   // setter for `minute'
}

 

20.5 추상 val 초기화

: 추상 val은 슈퍼 클래스에 빠진 자세한 부분을 서브 클래스에 전달할 수 있는 수단 제공

: 트레이트는 파라미터를 받을 생성자가 없기 때문에 트레이트에서 파라미터를 받으려면 서브 클래스에서 구현하는 추상 val을 이용한다.

trait RationalTrait { 
  val numerArg: Int       // 분자
  val denomArg: Int      // 분모

 

new RationalTrait {     // 트레이트를 혼합한 익명 클래스 인스턴스 => new Rational(1, 2)와 상응하는 효과
  val numerArg = 1
  val denomArg = 2
}

: new RationalTrait { ... } 와 new Rational(...) 은 비슷해 보이나 초기화 순서라는 중요한 차이 존재

 new Rational(expr1, expr2)


new RationalTrait {
  val numerArg = expr1
  val denomArg = expr2
}

: 클래스 Rational은 초기화하기 전에 expr1과 expr2 계산 => Rational 클래스의 초기화에서 사용가능

: 익명클래스를 초기화하는 도중에 표현식 expr1, expr2를 계산 => RationalTrait 다음에 초기화된다. => numerArg와 denomArg 값 사용 불가(디폴트 값이 들어 있다.)

trait RationalTrait { 
  val numerArg: Int 
  val denomArg: Int 
  require(denomArg != 0)
  private val g = gcd(numerArg, denomArg)
  val numer = numerArg / g
  val denom = denomArg / g
  private def gcd(a: Int, b: Int): Int = 
    if (b == 0) a else gcd(b, a % b)
  override def toString = numer +"/"+ denom
}

 

scala> val x = 2
x: Int = 2

scala> new RationalTrait {
     |   val numerArg = 1 * x
     |   val denomArg = 2 * x
     | }
java.lang.IllegalArgumentException: requirement failed     
   at scala.Predef$.require(Predef.scala:134)
   at RationalTrait$class.$init$(:8)
      at $anon$1.(:8)
      ...

 : RationalTrait 클래스를 초기화할 때 denomArg의 값이 디폴트 값인 0이기 때문에 require 호출이 실패한다.

 

=> 클래스 파라미터 인자는 클래스 생성자에 전달되기 전에 계산되고 서브클래스의 val 정의를 계산하는 것은 슈퍼 클래스를 초기화한 다음에만 이뤄진다.

=> 필드를 미리 초기화하거나 지연 val을 사용한다.

 

< 필드를 미리 초기화하기 >

: 슈퍼 클래스를 호출하기 전에 서브클래스의 필드를 초기화한다.

: 필드 정의를 중괄호에 넣어서 슈퍼 클래스 생성자 호출 앞에 위치시키면 된다.

" 익명 클래스 표현식에서 필드를 미리 초기화 "

scala> new { 
     |   val numerArg = 1 * x
     |   val denomArg = 2 * x 
     | } with RationalTrait
res1: java.lang.Object with RationalTrait = 1/2

 

" 객체 정의에서 필드를 미리 초기화 "

object twoThirds extends {
  val numerArg = 2
  val denomArg = 3
} with RationalTrait

 

" 클래스 정의에서 필드를 미리 초기화 "

class RationalClass(n: Int, d: Int) extends {
  val numerArg = n
  val denomArg = d
} with RationalTrait {
  def + (that: RationalClass) = new RationalClass(
    numer * that.denom + that.numer * denom,
    denom * that.denom
  )
}

: 미리 초기화한 필드는 슈퍼 클래스 생성자를 호출하기 전에 초기화되기 때문에 초기화 시 생성 중인 객체 언급 X

scala> new {
   |   val numerArg = 1
   |   val denomArg = this.numerArg * 2
   | } with RationalTrait
:9: error: value numerArg is not a member of object 
  $iw
       val denomArg = this.numerArg * 2

 

< 지연 계산 val 변수 >

: 어떤 val 정의 앞에 lazy 수식자가 있으면 프로그램에서 그 val 값을 처음 사용할 때 초기화한다. => 시스템이 스스로 어떻게 초기화할 지 결정한다.

scala> object Demo {
     |   val x = { println("initializing x"); "done" }
     | }
defined module Demo

scala> Demo
initializing x
res3: Demo.type = Demo$@17469af

scala> Demo.x
res4: java.lang.String = done

------------------------------------------------

 

scala> object Demo {
     |   lazy val x = { println("initializing x"); "done" }
     | }
defined module Demo

scala> Demo
res5: Demo.type = Demo$@11dda2d

scala> Demo.x
initializing x
res6: java.lang.String = done

: Demo를 초기화하는 과정에는 x를 초기화하는 과정이 들어가지 않는다. x의 초기화를 x가 맨 처음 쓰일 때까지 연기한다. 

 

" 트레이트를 지연 val로 초기화하기 "

trait LazyRationalTrait { 
  val numerArg: Int 
  val denomArg: Int 
  lazy val numer = numerArg / g
  lazy val denom = denomArg / g
  override def toString = numer +"/"+ denom
  private lazy val g = {
    require(denomArg != 0)
    gcd(numerArg, denomArg)
  }
  private def gcd(a: Int, b: Int): Int = 
    if (b == 0) a else gcd(b, a % b)
}

 

scala> val x = 2
x: Int = 2

scala> new LazyRationalTrait {
     |   val numerArg = 1 * x
     |   val denomArg = 2 * x
     | }
res7: java.lang.Object with LazyRationalTrait = 1/2

: 모든 구체적 필드는 지연 필드 

<초기화 순서>

1. LazyRationalTrait의 새 인스턴스를 만들고 초기화 코드를 실행한다. 초기화 코드에는 아무 것도 안 들어있다.

2. 익명 서브클래스의 주 생성자를 실행한다. numerArg와 denomArg가 초기화된다.

3. 인터프리터는 출력을 위해 toString을 호출한다.

4. toString에서 numer을 최초로 접근해 numer의 초기화 표현식을 계산한다.

5. 계산 과정에서 g에 접근에 numerArg와 denomArg에 접근한다.

6. toString에서 denom을 최초로 접근한다. 이 때 g를 다시 계산하지 않는다.

: g가 numer, denom 보다 위치상 뒤에 있으나 초기화는 먼저 진행 => 각 정의의 코드 순서는 중요하지 않다. 

=> 지연 val을 사용할 경우 프로그래머가 모든 val을 필요한 곳에서 참조할 수 있게 배치하려면 어떻게 해야 할지 고민 하지 않아도 된다.

 

: 단, 지연 val은 부수효과가 없는 함수형 객체와 어울린다. 부수효과가 있는 경우 초기화 순서가 문제가 되기 시작한다. 

 

20.6 추상 타입 

- 추상 타입 선언 : 서브 클래스에서 구체적으로 정해야 하는 어떤 대상에 대한 빈 공간을 마련해두는 것으로 선언 시점에서는 어떤 타입인지 알려져 있지 않은 타입을 참조하기 위해 사용

 

Ex. 동물의 음식 섭취

class Food
abstract class Animal {
  def eat(food: Food)
}

 

class Grass extends Food
class Cow extends Animal {
  override def eat(food: Grass) {} // Animal의 eat 메소드와 Cow의 eat 메소드 파라미터 타입이 다르기 때문에 컴파일 오류
}

: 타입 시스템이 불필요하게 엄격한 것으로 보일 수 있다. 어떤 메소드의 파라미터를 서브 클래스에서 특화하도록 허용한다면 아래 같은 상황이 나타난다.

class Food
abstract class Animal {
  def eat(food: Food)
}
class Grass extends Food
class Cow extends Animal {
  override def eat(food: Grass) {} // This won't compile,
}                                  // but if it did,...


class Fish extends Food
val bessy: Animal = new Cow
bessy eat (new Fish)     // ...you could feed fish to cows.

=> 더 명확하게 타입을 설정

class Food
abstract class Animal {
  type SuitableFood <: Food   
  def eat(food: SuitableFood)
}

 

class Grass extends Food
class Cow extends Animal {
  type SuitableFood = Grass
  override def eat(food: Grass) {}
}

 

scala> class Fish extends Food
defined class Fish

scala> val bessy: Animal = new Cow
bessy: Animal = Cow@2e3919

scala> bessy eat (new Fish)
:12: error: type mismatch;
   found   : Fish
   required: bessy.SuitableFood   // 객체와 멤버를 타입에 포함시킬 수 있다. 
         bessy eat (new Fish)
                    ^

 

20.7 경로에 의존하는 타입

: bessy.SuitableFood 같은 타입을 경로에 의존하는 타입이라고 부른다.(경로는 객체에 대한 참조)

: 경로가 다르면 타입도 달라진다.


class DogFood extends Food
class Dog extends Animal {
  type SuitableFood = DogFood
  override def eat(food: DogFood) {}
}


scala> val bessy = new Cow
bessy: Cow = Cow@e7bbeb

scala> val lassie = new Dog
lassie: Dog = Dog@ce38f1

scala> lassie eat (new bessy.SuitableFood)
:14: error: type mismatch;
   found   : Grass
   required: DogFood
         lassie eat (new bessy.SuitableFood)
                     ^


scala> val bootsie = new Dog
bootsie: Dog = Dog@66db21

scala> lassie eat (new bootsie.SuitableFood)

 

: 경로에 의존하는 타입은 자바의 내부 클래스 타입과 문법이 비슷하지만 경로 의존 타입은 외부 객체에 이름을 붙이는 반면, 내부 클래스 타입은 외부 클래스에 이름을 붙인다는 점이 다르다.

class Outer {
  class Inner
}
=> 내부 클래스를 자바는 Outer.Inner로 표현하고 스칼라는 Outer#Inner로 표현('.' 문법은 객체에만 사용된다.)

 

val o1 = new Outer 
val o2 = new Outer

=> o1.Inner와 o2.Inner은 경로 의존 타입이고 더 일반적인 타입 Outer#Inner의 서브 타입이다.

Outer#Inner은 Outer 타입의 임의의 외부 객체 내부에 있는 Inner 클래스를 의미

o1.Inner와 o2.Inner은 특정 외부 객체의 Inner 클래스를 의미

 

=> 내부 클래스 인스턴스는 그 인스턴스를 둘러싼 외부 클래스의 인스턴스를 가리키는 참조 존재

=> 내부 클래스 인스턴스에서 외부 클래스의 인스턴스를 지정해야 한다.

 

scala> new o1.Inner
res11: o1.Inner = Outer$Inner@1df6ed6

 

scala> new Outer#Inner
:7: error: Outer is not a legal prefix for
    a constructor
         new Outer#Inner
                   ^

 

20.8 세분화한 타입 

: 어떤 클래스 A가 다른 클래스 B를 상속받을 때 A가 B의 이름에 의한 서브타입이라고 말한다. 각 타입에 이름이 있고 서브 타입 관계를 선언하면서 각 클래스의 이름을 명시하기 때문

: 스칼라는 세분화한 타입을 통해 구조적인 서브 타이핑(타입 간의 관계가 타입 내부 구조에 의해서 결정)을 지원

: 이름에 의한 서브타입이 편하고 먼저 사용해야 한다.(간결하기 때문에)

 

Ex. 풀을 먹는 동물을 포함할 수 있는 Pasture 클래스

1. AnimalThatEatsGrass 라는 트레이트를 만들어 목초지에 들어갈 모든 클래스에 포함시킨다. 하지만 장황하다. Cow 클래스는 이미 자신이 동물이고 풀을 먹는다고 선언했다.(SuitableFood 타입을 통해)

2. AnimalThatEatsGrass을 선언하는 대신 세분화한 타입을 사용한다. 

class Pasture {
  var animals: List[Animal { type SuitableFood = Grass }] = Nil
  // ...
}

 

 

20.9 열거형

: 스칼라는 열거형을 위한 특별한 문법을 제공하지 않고 scala.Enumeration이라는 표준 라이브러리로 제공한다.

object Color extends Enumeration {
  val Red = Value
  val Green = Value
  val Blue = Value
}

 

object Color extends Enumeration {
  val Red, Green, Blue = Value
}

: 스칼라의 열거형은 경로 의존 타입의 예로서 Color.Red, Color.Green, Color.Blue의 타입은 Color.Value 이다.

object Direction extends Enumeration {
  val North = Value("North")
  val East = Value("East")
  val South = Value("South")
  val West = Value("West")
}

 

scala> for (d <- Direction.values) print(d +" ")
North East South West 

scala> Direction.East.id
res14: Int = 1

scala> Direction(1)
res15: Direction.Value = East

 

20.10 사례 연구

< Currency 클래스 설계 > : 달러, 엔, 유로 등 통화 단위로 일정 금액의 돈을 표현할 수 있다. 같은 단위의 통화를 표현하는 두 통화 인스턴스를 더할 수 있어야 한다. 어떤 통화에 이자율을 나타내는 값을 곱할 수도 있어야 한다. 

abstract class Currency {
  val amount: Long                        // 추상 필드
  def designation: String                 //  USD, Yen, Euro
  override def toString = amount +" "+ designation
  def + (that: Currency): Currency = ...
  def * (x: Double): Currency = ...
}

 

abstract class Dollar extends Currency {
  def designation = "USD"
}
abstract class Euro extends Currency {
  def designation = "Euro"
}

=>  Dollar 객체의 + 메소드에서 Euro 객체가 파라미터로 들어올 수 있다.

=>  추상 타입 사용

abstract class AbstractCurrency {
  type Currency <: AbstractCurrency
  val amount: Long
  def designation: String 
  override def toString = amount +" "+ designation
  def + (that: Currency): Currency = new Currency {  // 컴파일 오류 발생 

             // 스칼라는 추상 타입의 인스턴스를 만들 수 없고, 추상 타입을 다른 클래스의 슈퍼 타입으로 만들 수 없다.
    val amount = this.amount + that.amount
  }
  def * (x: Double): Currency = ...
}

abstract class Dollar extends AbstractCurrency {
  type Currency = Dollar
  def designation = "USD"
}

=> 추상 타입의 인스턴스를 직접 만드는 대신 추상 타입을 만들어내는 추상 메소드를 만들 수 있다. 

abstract class AbstractCurrency {
  type Currency <: AbstractCurrency // abstract type
  def make(amount: Long): Currency  // factory method
  ...                               // rest of class
}

=> Currency가 Currency를 만들 수 있다.

=> Currency를 만들 방법이 make 밖에 없는데 Currency 안에 make 존재 => 최초의 Currency 객체를 만들어낼 방법이 없다. => 다른 생성자를 추가할 수 있지만 본질적으로 make와 하는 일이 같다. 

 

=> 추상 타입과 팩토리 메소드를 밖으로 옮긴다.

 

abstract class CurrencyZone {
  type Currency <: AbstractCurrency
  def make(x: Long): Currency
  
  abstract class AbstractCurrency {
    val amount: Long
    def designation: String 
    override def toString = amount +" "+ designation
    def + (that: Currency): Currency = 
      make(this.amount + that.amount)
    def * (x: Double): Currency = 
      make((this.amount * x).toLong)
  }
}

 

object US extends CurrencyZone {
  abstract class Dollar extends AbstractCurrency {
    def designation = "USD"
  }
  type Currency = Dollar
  def make(x: Long) = new Dollar { val amount = x }
}

 

" 통화의 하위 단위 표현, 미국 달러의 하위 단위는 센트, amount 필드가 금액을 달러 대신 센트 단위로 저장 "

 
object US extends CurrencyZone {
  abstract class Dollar extends AbstractCurrency {
    def designation = "USD"
  }
  type Currency = Dollar
  def make(cents: Long) = new Dollar {
    val amount = cents
  }
  val Cent = make(1)         // 1센트 동전
  val Dollar = make(100)    // 1달러 지폐
  val CurrencyUnit = Dollar   // 표준 화폐 단위

}

 

----------------------------------------------

  override def toString =      // 10달러와 23센트의 합은 10.23 USD
  ((amount.toDouble / CurrencyUnit.amount.toDouble)
   formatted ("%."+ decimals(CurrencyUnit.amount) +"f")     // 자바의 String.format과 비슷
   +" "+ designation)

 

private def decimals(n: Long): Int = 
  if (n == 1) 0 else 1 + decimals(n / 10)

 

------------------------------------------------

 

object Europe extends CurrencyZone {
  abstract class Euro extends AbstractCurrency {
    def designation = "EUR"
  }
  type Currency = Euro
  def make(cents: Long) = new Euro {
    val amount = cents
  }
  val Cent = make(1)
  val Euro = make(100)
  val CurrencyUnit = Euro
}

object Japan extends CurrencyZone {
  abstract class Yen extends AbstractCurrency {
    def designation = "JPY"
  }
  type Currency = Yen
  def make(yen: Long) = new Yen {
    val amount = yen
  }
  val Yen = make(1)
  val CurrencyUnit = Yen
}

" 환율 계산 "

object Converter {
  var exchangeRate = Map(
    "USD" -> Map("USD" -> 1.0   , "EUR" -> 0.7596, 
                 "JPY" -> 1.211 , "CHF" -> 1.223),
    "EUR" -> Map("USD" -> 1.316 , "EUR" -> 1.0   , 
                 "JPY" -> 1.594 , "CHF" -> 1.623),
    "JPY" -> Map("USD" -> 0.8257, "EUR" -> 0.6272, 
                 "JPY" -> 1.0   , "CHF" -> 1.018),
    "CHF" -> Map("USD" -> 0.8108, "EUR" -> 0.6160, 
                 "JPY" -> 0.982 , "CHF" -> 1.0  )
  )
}

 

def from(other: CurrencyZone#AbstractCurrency): Currency = 
  make(math.round(
    other.amount.toDouble * Converter.exchangeRate
      (other.designation)(this.designation)))

+ Recent posts