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

- 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)))

타입 파라미터화 => 제네릭 클래스/트레이트 사용 가능

Ex. 집합은 제네릭이며 타입 파라미터를 받기 때문에 타입이 Set[T]고 T를 반드시 명시해야 한다.

        * 자바는 타입 파라미터를 쓰지 않아도 된다.  ( Ex. ArrayList a = new ArrayList<>(); )

 

19.1 함수형 큐

- 세 연산을 제공하는 데이터 구조

1. head : 큐의 첫 원소를 반환한다.

2. tail : 큐의 첫 원소를 제외한 나머지를 반환한다.

3. enqueue : 인자로 주어진 원소를 큐의 맨 뒤에 추가한 새로운 큐를 반환한다.

- 변경 가능한 큐와 달리 원소를 추가해도 내용을 바꾸지 않는다.

- 효율적 구현을 통해 리스트와 마찬가지로 head, tail, enqueue가 모두 상수 시간이 걸리도록 한다.

=> head와 tail은 리스트 표현 타입을 통해 상수 시간 구현 가능

class SlowAppendQueue[T](elems: List[T]) {   
  def head = elems.head
  def tail = new SlowAppendQueue(elems.tail)
  def enqueue(x: T) = new SlowAppendQueue(elems ::: List(x))   // 비효율적, 원소의 개수에 비례하는 시간
}

" enqueue 연산을 상수 시간에 하기 위해 리스트의 원소를 뒤집은 버전 " => head와 tail 비효율적

class SlowHeadQueue[T](smele: List[T]) { // Not efficient
  // smele is elems reversed
  def head = smele.last
  def tail = new SlowHeadQueue(smele.init)
  def enqueue(x: T) = new SlowHeadQueue(x :: smele)
}

=> 위 두 가지 방법을 합친다.

=> leading과 trailing 두 리스트로 구현해 leading 리스트는 앞부분부터, trailing 리스트는 뒷부분부터 거꾸로 저장

    전체 큐 내용은 leading ::: trailing.reverse

    원소 추가 시 trailing 리스트에 넣고 head나 tail 연산 전에 빈 leading 리스트에 trailing 리스트를 복사(mirror 메소드)

class Queue[T](
  private val leading: List[T], 
  private val trailing: List[T] 
) {
  private def mirror =      // leading 리스트가 비어 있는 경우에만 원소 개수에 비례, 아닐 경우 상수 시간
    if (leading.isEmpty)
      new Queue(trailing.reverse, Nil)
    else
      this

  def head = mirror.leading.head

  def tail = { 
    val q = mirror 
    new Queue(q.leading.tail, q.trailing) 
  }

  def enqueue(x: T) = 
    new Queue(leading, x :: trailing)
}

: tail 연산이 n번 호출될 때 mirror 메소드에서 n(원소 개수) 시간 소요 => 평균적으로 상수 시간

 

단, head/tail/enqueue가 거의 비슷한 빈도로 호출한다는 가정, head를 다른 두 연산보다 훨씬 더 많이 호출한다면, 매번 head를 실행할 때마다 비싼 비용을 들여 mirror 호출

 

19.2 정보 은닉

: Queue의 생성자에 누구나 접근할 수 있고 생성자 파라미터인 두 리스트 중 하나는 순서를 뒤집어야 하는 문제 => 클라이언트에게 코드를 감춰야 한다.

 

< 비공개 생성자와 팩토리 메소드 > => 클래스 초기화와 내부 표현을 감춘다.

class Queue[T] private (
  private val leading: List[T],
  private val trailing: List[T]

: 파라미터 목록 바로 앞에 private 수식자를 붙여서 주 생성자를 감출 수 있다.

scala> new Queue(List(1, 2), List(3))
:6: error: constructor Queue cannot be accessed in 
  object $iw
    new Queue(List(1, 2), List(3))

=> 주 생성자를 호출할 수 없기 때문에

1. 보조 생성자 추가

def this() = this(Nil, Nil) 

def this(elems: T*) = this(elems.toList, Nil)

2. 팩토리 메소드 추가

object Queue {
  def apply[T](xs: T*) = new Queue[T](xs.toList, Nil)   

      // Queue(1,2,3) => Queue.apply(1,2,3) : 메소드가 객체에 속해 있지만 전역 메소드를 호출한 것과 같은 효과
}

 

< 대안 : 비공개 클래스 > => 클래스 자체를 감추고 클래스에 대한 공개 인터페이스만을 제공하는 트레이트를 외부로 노출

trait Queue[T] {   
  def head: T
  def tail: Queue[T]
  def enqueue(x: T): Queue[T]
}

object Queue {

  def apply[T](xs: T*): Queue[T] = 
    new QueueImpl[T](xs.toList, Nil)

  private class QueueImpl[T](    // 구현 클래스 전체를 감춘다.
    private val leading: List[T],
    private val trailing: List[T]
  ) extends Queue[T] {

    def mirror = 
      if (leading.isEmpty)
        new QueueImpl(trailing.reverse, Nil)
      else 
        this

    def head: T = mirror.leading.head

    def tail: QueueImpl[T] = {
      val q = mirror
      new QueueImpl(q.leading.tail, q.trailing)
    }

    def enqueue(x: T) = 
      new QueueImpl(leading, x :: trailing)
  }
}

 

19.3 변성 표기 

: 위에서 정의한 Queue는 트레이트이며 타입이 아니다. 타입이 아닌 이유는 타입 파라미터를 받기 때문

scala> def doesNotCompile(q: Queue) {}   // Queue라는 타입의 변수를 만들 수 없다.
:5: error: trait Queue takes type parameters
     def doesNotCompile(q: Queue) {}
                        ^

scala> def doesCompile(q: Queue[AnyRef]) {}   

            // Queue 트레이트는 Queue[Int], Queue[AnyRef] 처럼 파라미터화된 타입을 지정하도록 허용한다.
doesCompile: (Queue[AnyRef])Unit

=> Queue는 트레이트이고 Queue[String]은 타입 => Queue는 타입 생성자

: 타입 생성자인 Queue는 Queue[Int], Queue[String] 같은 일련의 타입을 생성한다.

: Queue는 제네릭 트레이트   

    * 제네릭 : 여러 타입을 고려해 포괄적으로 작성한 클래스/트레이트, 여러 구체적인 타입을 정의할 수 있다.

    * 타입 파라미터를 받을 수 있는 클래스/트레이트를 제네릭, 그들이 만들어내는 타입은 파라미터화된 타입

=> Queue 트레이트는 제네릭 큐, Queue[Int]는 구체적 큐

 

: 스칼라에서 제네릭 타입은 기본적으로 무공변 

 => Queue에서 원소 타입이 각기 다른 큐 사이에는 결코 서브타입 관계가 성립되지 않는다.

       Ex. 위 deosCompile 메소드의 인자로 Queue[String]을 Queue[AnyRef] 대신 사용할 수 없다.

: 타입 파라미터 앞에 "+" 접두사를 붙이면 공변성을 요구할 수 있다.

trait Queue[+T] { ... }   

// 스칼라에게 Queue[String]을 Queue[AnyRef]의 서브타입으로 간주하라고 요구한다. 컴파일러는 Queue의 구현에서 해당 서브타입이 건전한지 검사한다.

: 타입 파라미터 앞에 "-" 접두사를 붙이면 반공변성을 요구할 수 있다.

trait Queue[-T] { ... }   

// T가 S의 서브타입인 경우 Queue[S]가 Queue[T]의 서브타입

* 파라미터의 변성 : 어떤 타입 파라미터의 공변, 반공변, 무공변 여부

* 변성 표기 : 타입 파라미터에 붙일 수 있는 +나 - 기호

 

* 순수 함수형 세계에서 여러 타입은 태생적으로 공변적이지만 읽고 쓸 수 있는 변경 가능한 데이터는 그렇지 않다.

class Cell[+T](init: T) {
  private[this] var current = init
  def get = current
  def set(x: T) { current = x }
}

 

Cell.scala:7: error: covariant type T occurs in
contravariant position in type T of value x
   def set(x: T) = current = x
              ^

 

val c1 = new Cell[String]("abc")
val c2: Cell[Any] = c1
c2.set(1)           // String을 받는 c1 인스턴스에 Int 타입을 넣는다. => 오류, 함수형 객체는 이런 경우 발생 X
val s: String = c1.get 

 

< 변성과 배열 >

: 자바는 배열을 공변적으로 다룬다.

String[] a1 = { "abc" };
Object[] a2 = a1;
a2[0] = new Integer(17);  // 컴파일은 잘 되지만 실행할 때 ArrayStore 예외 발생  
String s = a1[0];

: 자바는 실행 시점에 원소의 타입을 저장 => 안전 X

     * 제네릭이 나오기 전 배열을 제네릭하게 다룰 방법이 필요했기 때문, 제네릭이 생긴 이후에도 호환성 때문에 존재

         Ex.  void sort(Object[] a, Comparator cmp) { ... }

: 스칼라는 배열을 공변적으로 다루지 않는다.

scala> val a1 = Array("abc")
a1: Array[java.lang.String] = Array(abc)

scala> val a2: Array[Any] = a1
:5: error: type mismatch;
  found   : Array[java.lang.String]
  required: Array[Any]
      val a2: Array[Any] = a1

: 자바와의 호환을 위해 T 타입의 배열을 T의 슈퍼타입 배열로 변환 가능

// 컴파일 시 항상 합법적, 자바 언어와 마찬가지로 공변적으로 다룬다.

scala> val a2: Array[Object] =
         a1.asInstanceOf[Array[Object]]
a2: Array[java.lang.Object] = Array(abc)

 

19.4 변성 표기 검사

" Queue가 공변적이라고 가정 "

 

class StrangeIntQueue extends Queue[Int] {
  override def enqueue(x: Int) = {
    println(math.sqrt(x))
    super.enqueue(x)
  }
}

 

val x: Queue[Any] = new StrangeIntQueue
x.enqueue("abc")                // 올바르지 않음

 

=> 실제 공변성을 가진 Queue를 컴파일하면 (

class Queue[+T] {
  def enqueue(x: T) = 
   ...
}


Queues.scala:11: error: covariant type T occurs in
contravariant position in type T of value x
  def enqueue(x: T) = 
             ^

=> 제네릭 타입 파라미터가 메소드 파라미터의 타입이라면 그 메소드를 포함하는 클래스나 트레이트는 해당 타입 파라미터에 대해 공변적이지 않을 수 있다. 

 * 재할당 가능 필드도 마찬가지 ( var x : T 는 게터 메소드 def x: T와 세터 메소드 def x_=(y:T)로 취급 )

 

< 스칼라 컴파일러가 변성 표기를 검사하는 방법 >

: 컴파일러는 클래스 안에서 타입 파라미터를 사용하는 모든 부분이 긍정적/부정적/중립적인지 구분한다.

: +로 표시한 타입 파라미터는 긍정적인 위치에서만 사용 가능, -로 표시한 파라미터는 부정적 위치에서만 사용 가능, 아무 변성 표기가 없는 타입 파라미터는 아무 곳에서나 사용 가능

: 가장 바깥 스코프부터 내부 스코프로 긍정적/부정적/중립적인지 구분한다.

 * 기본적으로 내부 스코프는 바깥 스코프와 구분이 같다.

 * 메소드 값 파라미터, 메소드의 타입 파라미터는 바깥 스코프 구분과 반대

 * 어떤 타입의 타입 인자의 구분은 해당 타입 파라미터의 변성에 따라 구분을 반대로 뒤집을 지 결정

       Ex C[Arg]의 Arg에 +표기가 붙어 있으면 구분을 그대로 유지, -표기가 붙어 있으면 현재의 구분과 반대

abstract class Cat[-T, +U] {
  def meow[W-](volume: T-, listener: Cat[U+, T-]-)
    : Cat[Cat[U+, T-]-, U+]+
}

 

19.5 하위 바운드

: Queue[T] 정의에서 T를 공변적으로 만들 수 없다. => T는 enqueue 메소드의 파라미터 타입인데 그 위치는 부정적

=> enqueue를 다형성(즉 enqueue에 타입 파라미터를 지정)으로 더 일반화하고, 타입 파라미터에 하위 바운드를 사용

class Queue[+T] (private val leading: List[T],
    private val trailing: List[T] ) {
  def enqueue[U >: T](x: U) = 
    new Queue[U](leading, x :: trailing) // ...
}

 

class Fruit
class Apple extends Fruit
class Orange extends Fruit

 

class StrangeQueue extends Queue[Apple](Nil, Nil ) {
  override def enqueue[Fruit>:Apple](x: Fruit) = {
    super.enqueue(x)
  }
}

 

val x: Queue[Fruit] = new StrangeQueue 

x.enqueue(new Orange)

Ex. Fruit 클래스에 두 서브 클래스 Apple, Orange가 있을 때, Orange를 Queue[Apple]에 추가할 수 있다. 추가한 Queue의 타입은 Queue[Fruit]가 된다. => 더 일반적

 

: 자바는 사용자 위치 변성 => 어떤 클래스 설계가 단독으로 존재하고 실제 와일드 카드를 채워 넣는 것은 그 클래스를 사용하는 쪽이다. => 사용자 쪽에서 타입 다룰 때 조심(사용자 쪽에서 잘못 사용하면 중요한 인스턴스 메소드를 더 이상 사용할 수 없다.)

class Test<T>{}

public class Sample {
   public static void main(String[] args) {
      Test<? extends String> test = new Test();
   }
}

: 스칼라는 선언 위치 변성 => 설계할 때 타입을 넣음으로써 사용자가 타입으로 인한 실수를 하지 않도록 하고 컴파일러에서 메소드를 사용 가능한지 확인해준다.(타입 위주 설계)

 

19.6 반공변성

" 반공변 출력 채널 "

trait OutputChannel[-T] {
  def write(x: T)
}

: OutputChannel[AnyRef]의 출력 채널은 OutputChannel[String]을 출력하는 채널의 서브 타입

: OutputChannel[AnyRef] 채널에는 아무 객체나 쓸 수 있지만 OutputChannel[String] 채널에는 오직 문자열만 쓸 수 있다. => OutputChannel[AnyRef]가 필요한 곳에 OutputChannel[String]을 바꿔 넣는 것은 안전 X

 

=> U 타입의 값이 필요한 모든 경우를 T 타입의 값으로 대치할 수 있다면, T 타입을 U 타입의 서브타입으로 가정해도 안전 : 리스코프 치환 원칙 

=> T가 U의 모든 연산을 지원하고, 모든 T의 연산이 그에 대응하는 U의 연산에 비해 요구하는 것은 더 적고 제공하는 것은 더 많은 경우 리스코프 치환 원칙이 성립

 

Ex. OutputChannel[AnyRef]와 OutputChannel[String]이 같은 write 연산을 제공하고 OutputChannel[AnyRef]에 있는 연산이 OutputChannel[String]에 비해 더 적은 것을 요구하기 때문에 리스코프 치환 원칙 성립(더 적다는 건 전자는 단지 AnyRef만을 요구하는 반면, 후자는 제약이 더 많고 구체적인 String을 요구한다.)

 

" 한 타입 안에 공변성과 반공변성이 섞여 있는 함수 트레이트 "

trait Function1[-S, +T] {
  def apply(x: S): T
}

: 인자는 함수가 요구하는 것, 결과는 함수가 제공하는 것 => S는 반공변, T는 공변

 

class Publication(val title: String)
class Book(title: String) extends Publication(title)

object Library {
 val books: Set[Book] =
   Set(
     new Book("Programming in Scala"),
     new Book("Walden")
   )
 def printBookList(info: Book => AnyRef) {
   for (book <- books) println(info(book))
 }
}

object Customer extends App {
 def getTitle(p: Publication): String = p.title
 Library.printBookList(getTitle)
}

 

 

19.7 객체의 비공개 데이터

: Queue에서 leading이 비어있는 데 head를 여러 번 호출했을 때 mirror 연산이 trailing 리스트를 leading 리스트로 반복해서 복사하는 문제 => 부수 효과를 활용해 개선

 class Queue[+T] private (
   private[this] var leading: List[T],             // 재할당 가능한 var 변수 => 동반 객체에도 보이지 않는다.
   private[this] var trailing: List[T]              // 재할당 가능한 var 변수
 ) {  

   private def mirror() = 
     if (leading.isEmpty) {
       while (!trailing.isEmpty) {
         leading = trailing.head :: leading            // 현재의 큐 변경
         trailing = trailing.tail                            // 현재의 큐 변경
       }
     }

   def head: T = { 
     mirror()
     leading.head 
   }

   def tail: Queue[T] = { 
     mirror()
     new Queue(leading.tail, trailing) 
   }

   def enqueue[U >: T](x: U) = 
     new Queue[U](leading, x :: trailing)
 }

: leading, trailing이 비공개 변수여서 부수효과는 순전히 Queue 내부에서만 나타난다. => 순수 함수형 객체

: private[this]는 그들이 정의된 객체에서만 접근 가능해 변성에 아무 문제를 일으키지 않는다.(변성이 타입 오류를 발생시키는 경우를 만들어내려면 어떤 객체가 정의되는 시점의 타입보다 정적으로 더 약한 타입의 객체에 대한 참조가 객체 내부에 필요하다/ 하지만 객체 비공개 값에 접근할 경우 이런 일이 불가능하다.)

=> 타입 파라미터를 사용하는 부분이 긍정적/부정적/중립적인지 private[this]를 제외하고 검사한다.

 

19.8 상위 바운드

" Ordered 트레이트를 혼합한 Person 클래스 "

class Person(val firstName: String, val lastName: String)
    extends Ordered[Person] {

  def compare(that: Person) = {
    val lastNameComparison =
      lastName.compareToIgnoreCase(that.lastName)
    if (lastNameComparison != 0)
      lastNameComparison
    else
      firstName.compareToIgnoreCase(that.firstName)
  }

  override def toString = firstName +" "+ lastName
}

 

" 상위 바운드를 사용한 병합 정렬 - Ordered 트레이트를 믹스인한 모든 타입에 대해 정렬 수행 가능 "

 * Int는 Ordered[Int]의 서브타입이 아니기 때문에 제한 

def orderedMergeSort[T <: Ordered[T]](xs: List[T]): List[T] = {
  def merge(xs: List[T], ys: List[T]): List[T] =
    (xs, ys) match {
      case (Nil, _) => ys
      case (_, Nil) => xs
      case (x :: xs1, y :: ys1) =>
        if (x < y) x :: merge(xs1, ys)
        else y :: merge(xs, ys1)
    }
  val n = xs.length / 2
  if (n == 0) xs
  else {
    val (ys, zs) = xs splitAt n
    merge(orderedMergeSort(ys), orderedMergeSort(zs))
  }
}

 

* 스칼라는 비즈니스 로직에 초점을 맞추지 않는다.(내부 비즈니스 로직은 고차함수를 통해 미리 제공되어 있다.) 타입에 초점을 맞춰야 한다. 그래서 클래스나 트레이트를 쉽게 정의할 수 있는 방법 존재 => 설계의 시작은 타입

* 믹스인 트레이트는 기능에 불과 => 믹스인 트레이트 타입 대신 상위 바운드 사용

+ Recent posts