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

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