Page tree
Skip to end of metadata
Go to start of metadata

책 내용 정리

19장. 타입 파라미터화 

  • chapter13에서 예제로 설명되었던 purely functional queue를 위한 클래스의 디자인을 이용해서 설명한다.

  • type parameterization은 generic한 class와 trait을 구현할 수 있도록 해준다.

    • 예를 들어 set은 generic이고 type parameter를 가진다: Set[T]와 같이 설정할 수 있다.

    • 결과적으로 Set[String], Set[Int]와 같이 instance를 만들어서 쓸 수 있다. 

    • 하지만 Set은 어쨌든  간에 something의 set이어야 한다. => 타입 파라미터의 정의가 필요하다.

  • raw type을 지원하는 java와 달리 scala는 type parameter를 정의해야 한다. 

  • variance는 파라미터화된 타입의 상속 관계를 정의한다. ex) Set[String], Set[AnyRef]

  • 이 챕터는 세가지 파트로 구성되어 있다.

    • purely functional queue를 위한 데이터 구조 개발

    • 이 구조의 내부 representaion detail을 숨기는 테크닉

    • type parameter의 variance와 어떻게 information hiding과 상호작용하는 지 


19.1 함수형 큐 

  • 이 장에서 소개하는 Functional Queue의 요구사항
    • functional queue는 세가지 operation으로 구성되었다.
      • head : 큐의 첫번째 요소를 리턴
      • tail : 첫번째 값을 제외한 큐를 리턴
      • enqueue : 끝에 요소를 추가한 새로운 큐를 리턴
    • immutable하다
    • type관계없이 쓸 수 있다.
    • 리스트와의 차이
      • 비슷한 점
        • fully persistent data structure
          • 변경을 하던 확장을 하던 old version이 계속 사용가능하다.
      • 다른 점
        • list는 앞에도 요소 추가가 가능하지만 이 큐는 enqueue를 통해 맨 뒤에만 추가된다.
  • 이걸 어떻게 효과적으로 처리할 수 있을까??
    • 이상적으로 funcational(immutable) queue는 기본적으로 imperative style보다 기본적으로 더 높은 overhead를 가져서는 안된다. => 이 말은 head, tail, enqueue가 constant time에 동작해야 한다는 것을 의미한다
  • 가장 간단한 접근은 representation type으로 list를 이용해서 functional queue를 구현하는 것이다.
    • list에 있는 동작을 그대로 사용하고 enqueue는 concatenation을 사용한다.
    • 문제점은 enqueue operation이다. 
      • 이 경우 큐에 저장되는 엘리먼트의 수가 time proportional이다. 
      • reverse로 처리할 수 있긴 하지만 이렇게 되면 enqueue는 constant time인데..head와 tail이 time proportional이다.
        • page 386에 있는 예제는 enqueue가 time proportional이고...
        • page 387에 있는 예제는 head, tail이 time proportional이다.
  • 해결방법은 이러한 아이디어를 조합해서..reverse queue와 inverse queue를 동시에 들고 조작하면 된다.
    • leading: inverse queue
    • trailing: reverse queue
    • enqueue시에는 trailing에만 쌓고, head, tail시에 trailing을 trailing을 reverse해서 사용한다.
    • mirror가 leading이 empty인 경우 time proportional이기 때문에…mirror를 호출하는 head와 tail도 time proportional이다.
    • 하지만 리스트가 길어지면 질 수록 mirror에서 복사하는 동작이 덜 호출된다.
      • n의 길이를 가지고 empty leading list를 가진 큐가 있다고 가정해보자..
      • mirror는 n의 길이에 대해서 reverse-copy를 한번 할것이다.
      • 하지만 다음에는 n번의 tail operation이 지나야 호출될 거다..
        •  이건 n개의 tail operation에 대해서 mirror가 1/n의 complexity를 가진다는 것이고 이는 constant amount를 의미한다. 
        • 만약 head, tail, enqueue가 잘 분산되었다면 각 operation은 constant할 것이다. 
        • 그래서 functional queue는 mutable한 애보다..더 asymptotically(데이터가 많아질수록)하게 효율적이다.
  • 요렇게 내린 결론에는 몇가지 주의할 점이 있다.
    • 위의 이야기는 asymptotic한 행동에 대한 것이지..constant factor는 다소 다를 수 있다.
    • head, tail, enqueue가 같은 frequency로 호출되어야 하는 것은 논쟁의 여지가 있다.
      • head가 다른 것들보다 많이 호출되는 것은 상관이 없지만 head를 호출하기 위해 mirror로 list를 reorganization하는 것은 비용이 좀 든다.
      • 이런 부분은 피할 수 있는 데 head operation이 맨처음에만 re-organization으로 하기 위해서 Functional queue를 디자인할 수 있도록 한다. 


19.2 정보 은닉 

  •  19.1에서 보여진 큐는 효율적인 관점해서는 꽤 좋다. 하지만 효율성을 위해서 너무 많은 구현부가 노출되었다...
    • 생성자는 globally하게 접근 가능해야하고, 두개의 parameter를 받으며..그 중 하나는 reverse이다..
  • 이제 필요한건 client code로부터 이 생성자를 숨기는 것이다. 
    • Private constructors and factory methods
      • 자바에서는 private로 생성자를 숨길 수 있다. 
      • 스칼라에서는 primary constructor가 explicit한 definition을 가지지 않고 class parameter와 body에 의해서 implicit하게 정의된다.
      • 그럼에도 불구하고 여전히 클래스 파라미터 리스트의 앞에 private를 붙이면 primary constructor를 숨길 수 있다..
        • class Queue[T] private () => 이렇게 정의하면 오직 클래스 내부와 companion object에서만 접근할 수 있다. 
        • 이제..Queue class는 여전히 public이므로 type으로 사용Queue class의 primary constructor는 더 이상 client code에서 호출되지 않는다.
      • 그러면 new queue를 만들 수 있는 새로운 방법이 필요하다..
        • 초기 element의 sequence와 같은 것으로부터 queue를 만들어내는 factory method를 추가하면 되겠지!!
        • 이 factory method를 companion object의 apply method에 추가하면 Queue(1,2,3)과 같이 사용할 수 있다!!
        • 이러면 생성자가 private이지만 Queue를 생성할 수 있다!!
      • Scala에는 globally visible method가 없지만 요런 방식을 사용하면 global method를 사용하는 것처럼 보일 수 있다.
    • 대안: private class
      • private 생성자와 private member는 클래스의 initialization과 representation을 숨기기 위한 방법 중 하나이다.
      • Queue를 public하게 쓰기 위해 trait으로 빼고 전체 구현 클래스를 private class로 숨겼다.(Listing 19.4)


19.3 변성 표기 

  • Listing 19.4에 정의되는..Queue는 trait이지..type이 아니다..
    • 왜냐하면 Type parameter를 받기 때문이다.
    • 결과적으로 Queue type의 변수를 생성할 수 없다.
  • 여기서 trait Queue는 type constructor이다.
    • trait Queue는 parameterized type를 정의할 수 있다. ex) Queue[String], Queue[Int]
    • Queue type constructor는 Queue[Int], Queue[String]등의 type의 family들을 만들 수 있다.
  • Queue는 generic trait이기도 하다. 
    • 여기서 generic은 하나의 일반적인 class나 trait으로 여러 개의 specific type을 정의할 수 있다는 뜻이다.
  • 이런 type parameter와 subtyping의 조합은 몇가지 재미있는 질문을 낳는다.
    • Queue[T]에 의해서 생성된 녀석들 간에는 어떤 subtyping 관계가 있는가.. 
      • ex) Queue[String]은 Queue[AnyRef]의 subtype인가?
    • 더 generic하게 말하자면 S가 T의 subtype이라면, Queue[S]는 Queue[T]의 subtype인가?
      • 만약 그러하다면..trait Queue는 type parameter T에 대해 covariant(or flexible)하다고 말할 수 있다.
      • 혹은 하나의 type parameter만 가지고 있기 때문에 그냥 간단하게 Queue는 covariant하다고 말할 수 있다.
  • covariant(공변?)
    • 예를 들면 doesCompile 예제라고 한다면 Queue[String]을 전달했을 때 Queue[AnyRef]로 알아서 쓸 수 있는 것이다.
    • 이렇게 보면 당연히 String이 AnyRef의 concrete이므로 될 것 같아보인다.
    • 하지만 scala에서 generic type들은 default nonvariant(or, “rigid”) subtyping을 한다.
      • 이 말은 서로 다른 element type의 큐들은 subtype 관계를 전혀 가지지 않는다는 것을 의미한다.
      • 그래서 Queue[String]은 Queue[AnyRef]로 쓸 수 없다.
    • 그럼에도 불구하고 covariant subtyping을 하겠다면..다음과 같이 쓰면 된다.
      • trait Queue[+T]
        • +를 앞에 붙이면 이 parameter에 대해서 subtyping이 covariant라는 것을 가리킨다. 
        • 이는 Scala한테 Queue[String]을 Queue[AnyRef]의 subtype으로서 사용하고 싶다고 알려주는 것이다.
        • 따라서 이렇게 설정되어 있다면 컴파일러가 subtyping 관계가 성립하는 지를 체크한다.
      • trait Queue[-T]
        • -를 앞에 붙이면 contravariabnt subtyping이다.
        • T가 S의 subtype이면, Queue[S]는 Queue[T]의 subtype이다.
  • parameter’s variance는 다음과 같다.
    • covariant
    • contravariant
    • nonvariant
  • +,-같은 녀석들은 variance annotations
  • purely functional world에서 많은 type들은 자연스럽게 covariant이다.
    • 근데 mutable data를 사용하기 시작하면 상황이 바뀐다. 
    • Listing 19.5 예제를 하다보면..Cell[String]에 int값을 넣는 상황이 발생한다. 
    • 이건 명백하게 type soundness를 해치는 것이다.
    • 그리고 이 문제는 명백하게 covariant subtyping에 의한것이다.
    • 왠만하면 mutable하게 만들지 말자!!!
  • variance and arrays
    • 이러한 동작을 자바에서의 array의 행동과 비교해보는 것은 흥미롭다.
    • array는 java에서 covariant로서 다루어진다.
    • 395page의 예제에 따르면...자바에서는 이게 compile은 되지만 exception이 난다. 먼일일까??
      • java에서는 array의 element type을 runtime 시에 저장하고, array의 element가 업데이트 될 때마다 stored type과 비교를 해서 해당 type의 instance가 아니면 ArrayStore exception을 던진다.
    • 왜 자바에서 이렇게 unsafe하고 expensive한 방법을 적용했을까?
      • 제임스 고슬링한테 물어보니..그들은 array를 generically하게 다룰 수 있는 간단한 방법을 원했다고 한다.
        • 특히 sort를 쓸 때 void sort(Object[] a, Comparator cmp) {...}와 같이 사용하고 싶었다고 한다.
        • 이렇게 하면 어떤 타입이든 sort할 수 있으므로...
      • 머..이제는 java에도 generic이 들어가서 sort에 넘기기 위한 array의 covariance는 불필요해졌다.
    • 스칼라는 array를 covariant로 다루지 않음으로써 java보다 더 pure하려고 노력했다. 
      • 396page보면 알겠지만 scala는 array를 nonvariant로 본다.
      • 따라서 Array[String]은 Array[Any]가 아니다.
    • 하지만 살다보면 기존 자바코드를 Java를 사용할 수도 있다.
      • sort 메소드를 사용하고 싶을 수도 있고..
      • 이를 위해 Scala는 T의 array를 T의 어떤 subtype으로도 전환할 수 있게 해준다. 

        val a2: Array[Object] = a1.asInstanceOf[Array[Object]]


19.4 변성 표기 검사(잘 몰라요~~)

  • 이제 아마 variance가 이상한 몇몇 예제를 봤기 때문에 어떤 종류의 class definition이 reject되거나 accept되는 지 궁금할 거다.
    • type soundness의 모든 violation은 몇몇 reassignable field나 array element를 가지고 있다.
    • 반면에 queue의 purely functional implementation은 covariance의 좋은 예이다.
  • 다음 예제는 reassignable field가 없더라도 unsound situation을 만들어낼 수 있다는 것을 보여준다.
  • 이 예제는 mutable field도 없다.
  • generic parameter type이 method의 parameter로서 나타나자마자, containing class나 trait이 type parameter에서 covariant가 아니게 된다.
  • Reassignable field는 method parameter type으로서 사용되는 +의 type parameter를 disallow하는 rule의 특별한 케이스이다.
  • variance annotation의 정확성을 검증하기 위해서 Scala compiler는 positive, negative, neutral로서 trait body나 class에서의 모든 위치를 정의한다.
  • “position”은 type parameter가 사용되는 class body의 any location이다.
  • 모든 method value parameter는 position이다. 
    • 왜냐하면 method value parameter는 type을 가지므로 type parameter가 해당 position에서 보여질 수 있다.
  • 컴파일러는 class의 type parameter 각각의 사용을 체크한다.
    • +와 함께 사용된 type parameter는 positive position에서만 사용된다. 
    • -와 함께 사용된 type parameter는 negative position에서만 사용된다.


19.5 하위 바운드 

  • Queue 클래스로 돌아와서 Listring 19.4에서 보여진 Queue[T]는 T에 대해 covariant할수 없다. 
    • T가 enqueue method의 파라미터의 type으로서 나타났고, negative position이기 때문이다.
    • enqueue를 generalize해서 polymorphic하게 하고 type parameter를 위한 lower bound를 사용할 수 있는 방법이 있다.
  • Listing 19.6 예제
    • “U >: T” U의 lower bound는 T이다.
      • U는 T의 supertype이어야 한다.
    • Fruit이 Apple과 Orange의 subclass를 가진다고 하면 새로운 Queue는 Orange를 Queue[Apple]에 집어넣을 수도 있고..Queue[Fruit]을 리턴받는다.
  • Listing 19.6은 이전버전보다 더 낫다. => 더 general하기 때문.
    • 요렇게 정의해줌으로써 일반적으로 자연스럽게 서로 다른 element type의 큐를 모델링할 수 있다.
    • 이 예제는 variance annotation과 lower bound의 잘 어울리는 모습을 보여준다.
  • Listing 19.6은 인터페이스의 type이 자세한 디자인과 구현을 이끌어낸 type-driven design의 좋은 예이다.
    • 아마 lower bound를 이용한 구현을 생각하지 못했지만 covariant로 구현은 하려고 했다고 생각해보자. 
    • 그러면 compiler가 enqueue의 variance error를 알려줄 것이다.
    • variance error를 고치기 위해서 lower bound를 추가한 것이 더 general하고 유용한 queue를 만들게 했다.
    • 이러한 부분은 Scala가 declaration-site variance를 wildcard를 사용하는 use-site variance보다 더 선호하는 중요한 이유이다.
      •  use-site variance는 사용자들이 wildcard를 넣어줘야 하고 문제가 발생하면 아예 그 함수를 못쓴다..
      • variance는 좀 어려워서 사용자들이 잘못쓰는 경우가 많고 그러면 그 사람들은 wildcard와 generics가 너무 어렵다고 생각하게 될 것이다.
      • definition-side variance는 compiler에게 의도를 잘 표현하고 compiler는 원하는 메소드가 사용가능한지에 대해서 double check를 한다. 


19.6 반공변성 

  • contravariance가 자연스러운 경우가 있다.
  • Listing 19.7을 보자

    trait OutputChannel[-T] {
    	def write(x: T)
    }
    • OutputChannel이 T의 contravariant이다.
    • AnyRef의 output channel은 String의 output channel의 subtype이 된다.
      • 이게 말이 안되는 것 같아도 말이 된다..
      • OutputChannel[String]라고 한다면 허용되는 동작은 String을 쓰는 것 뿐이다.
      • 그런데 같은 동작(String을 쓰는 것)이 OutputChannel[AnyRef]에서도 가능하다..
      • 그래서 OutputChannel[String]을 OutputChannel[AnyRef]로 바꾸는 것은 안전하다.
      • 하지만 이와 대조적으로 OutputChannel[AnyRef]가 필요한 곳에 Output[Channel]을 사용하는 것은 위험하다.
        • 왜냐하면 OutputChannel[AnyRef]는 아무 객체나 쓸수 있지만..OutpuChannel[String]은 string만 쓸수 있기 때문이다.
  • Liskov substitution principle
    • 만약 type U의 값이 필요한 곳마다 type T로 대체할 수 있다면 type T는 type U의 subtype이라고 가정하는 것은 안전하다.
    • 따라서 만약 T가 U로써 같은 operation을 지원하고 T의 모든 operation이 U에서 대응되는 operation들보다 덜 요구하고 더 제공한다면 이 법칙이 지켜진다고 볼 수 있다.
    • Output channel의 경우 OutputChannel[AnyRef]는 OutputChannel[String]의 subtype이다.
      • 왜냐하면 둘 다 같은 write operation을 제공하고 이 operation은 OutputChannel[String]에서보다 OutputChannel에서[AnyRef]에서 덜 요구된다.
  • 때때로 covariance와 contravariance가 같은 타임에서 mix될때가 있다.
    • 예제는 Scala의 function trait
      • 예를 들어 function A => B를 작성했다고 치자..
      • 스칼라가 Function1[A, B]로 확장한다.
      • Function1의 정의는 covariance와 vontravariance를 같이 사용한다.
      • trait Function1[-S, +T] {
          def apply(x: S): T
        }
      • Listing 19.9 예제 살펴보자.

19.7 객체의 비공개 데이터 

  •  이전버전에서는 몇번씩 반복적으로 copy가 발생했다. => 이런 건 사이드 이펙트를 만들어 낼 수 있다.
  • Listing 19.10
    • 변한 점
      • leading과 trailing이 reassignable 변수
      • mirror가 새로운 큐를 주는 대신에 side effect를 가진다. 
      • side effect는 내부에서만 일어나고 Queue의 client에게 보여지지 않는다. 
      • 따라서 18장의 정의에 따라 새로운 버전의 Queue도 purely functional object이다.(var를 썼음에도 불구하고..)
  • Queue가 covariant parameter type T의 reassignable field를 두개 가지고 있는 데..variance rule 위반 한거 아니냐..
    • private[this]를 써서 object private를 만들어서 괜찮다.
    • object private member는 정의된 object 내에서만 접근이 가능하다.
    • 같은 객체로부터의 변수 접근은 variance의 문제를 발생하지 않는다.
      • variance가 type error를 만드는 경우를 만들기 위해서는 object가 정의한 타입보다 정적으로 더 약한 타입을 가지는 객체에 대한 참조를 가지고 있어야만 한다.
      • 하지만 private[this]의 경우 이것이 불가능하다.
  • variance checking rule은 object private definintion에 있어 특별한 경우를 포함한다. 
    • 이런 definition들은 같은 variance classfication을 가지는 위치에 +나 -같은 annotaion의 type이 있는 지 체크할 때마다 생략된다.
    • 그래서 Listing 9.10의 code가 compile error가 나지 않는다. 
    • 반면에 private에 this를 빼면 type error를 낼 것이다.

19.8 상위 바운드 

  •  upper bound는 <:를 써서 정의된다
    • ex) T <: Ordered[T] 
      • type 파라미터 T가 upper bound로 Ordered[T]를 가진다는 뜻.
      • orderedMergeSort에 전달되는 list의 element type은 Ordered의 subtype이어야 한다는 뜻이다.

19.9 결론 

20장. 추상 맴버

  • 멤버가 클래스에 완벽한 정의를 가지고 있지 않으면 class나 trait의 멤버는 abstract이다.
  • abstract member는 해당 클래스의 subclass에서 구현될 것으로 의도된 것이다. 
    • 이 아이디어는 많은 객체 지향 언어에서 찾을 수 있다.
  • 스칼라는 자바의 full generality를 넘어선다. 
    • 메소드에 추가적으로 abstract fields를 선언할 수 있을뿐만 아니라 class와 trait의 멤버로서 abstract type도 정의할 수 있다.
 

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

trait Abstract {
  type T
  def transform(x: T): T
  val initial: T
  var current: T
}
Abstract를 상속하는 녀석은 abstract member의 각 정의들을 채워넣어야 한다.

 

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

20.2 타입 멤버 

  • 스칼라에서의 abstract type
    • 정의를 하지 않고 class나 trait의 멤버로 정의된 type을 의미한다
    • 항상 다른 class나 trait의 멤버여야 한다.
  • type member를 사용하는 이유
    • type을 위한 짧고 이해가 쉬운 alias를 정의하기 위해서이다.
      • 그와 같은 type member는 class나 trait의 코드를 명확하게 하는 것을 돕는다.
    • subclass에서 정의되어야 하는 abstract type들을 정의하기 위해서 사용한다.
      • 이러한 사용은 뒤에서 더 자세히 설명한다. 


20.3 추상 val 변수 

  • val을 보면 타입만 정의하고 value정의를 하지 않았다.
  • 그렇다 할지라도 이 변수는 unchangeable 한 값이라는 건 알 수 있다.
  • Listing 20.1의 예제
    • def는 val이 될 수 있지만..val은 def가 될 수 없다. => 생각해보면 당연하다. 


20.4 추상 var 

  • abstract val과 같이 이름과 타입만 정의되고 초기값이 정의되지 않은 것이다.
  • abstract vars의 의미는 멀까? 
    • 18.2에서 봤다시피 클래스의 멤버인 vars는 getter와 setter method를 만든다. 
  • 그래서 Listing 20.2는 20.3과 완전히 동일하다. 
  • [Listing 20.2]
    trait AbstractTime {
      var hour: Int
      var minute: Int
    }
     
    [Listing 20.3
    trait AbstractTime {
      def hour: Int
      def hour_=(x: Int)
      def minute: Int
      def minute_=(x: Int)
    }


20.5 추상 val 초기화 

  • Abstract val은 때때로 superclass parameter와 동일한 역할을 할 때가 있다.
    • superclass에서 빠져있는 것들을 subclass에서 제공할 수 있도록 해준다.
  • 이게 trait의 경우 특히 중요한데 trait은 parameter를 받는 constructor가 없기 때문이다. 
    • 그래서 parameterizing trait을 사용하는 방법은 subclass에 구현된 abstract val을 통하는 것이다.
    • trait에 new를 붙이면 anonymous class를 만든다. 
  • 요걸로 보아 initialization order가 class parameter와 abstract filed는 다르다.
    • new Rational(expr1, expr2)의 경우 expr1과 expr2는 Rational을 생성하기 전에 evaluate된다.
    • 근데 anonymous이 경우에는 RationalTrait 이후에 초기화된다.
    • 그래서 numerArg와 denomArg의 값은 RationalTrait이 초기화될 때까지 available하지 않다.
    • 요건 Listing 20.4에서 문제가 된다.
      • class parameter는 class 생성자에게 넘겨지기 전에 evaluate된다
      • val의 구현은 superclass가 initialize된다음에 evaluate된다.
  • 그러면 요걸 걱정안하고 쓰려면 어떻게 해야할까??
    • pre-initialized fields
      • superclass가 호출되기 전에 subclass의 field를 초기화하게 해준다. 
        • 이를 위해서는 간단하게 field 정의를 superclass constructor 호출 앞에다가 하면 된다.
        • anonymous class에도 subclasss 방법에도 사용할 수 있다.
      • 단점
        • super class의 생성자가 호출되기 전에 초기화되기 때문에 초기화하는 코드가 object를 참조할 수 없다.
    • lazy val
      • 시스템이 알아서 초기화해야할때 초기화하는 것을 선호할 수도 있다.
      • val앞에 lazy를 붙이면 right-hand의 expression의 초기화는 val이 처음 사용될 때 evaluate된다.
      • 예제는 그거에 대한 설명.
      • 요건 parameter 없는 def를 쓰는 것과 비슷하긴 한데..다른 점은 lazy val의 경우 한번만 evaluate된다는 것이다. def는 매번 evaluate된다.
      • Listing 20.8의 경우 순서는 다음과 같다.
        • 1. LazyRationalTrait의 instance가 생성되고 LazyRationTrait의 초기화 코드가 동작한다. 초기화 코드가 비어있으므로 어떤 field도 초기화되지 않는다.
        • 2. new에 의해서 정의된 anonymous subclass의 primary 생성자가 실행된다. 이는 numerArg를 2로 만들고, denomArg를 4로 넣는다.
        • 3. 생성된 object의 toString 함수가 interpreter에 의해서 호출된다. 
        • 4. 호출된 toString에서 numer를 쓰므로 numer를 초기화한다.
        • 5. numer의 초기화코드는 private field g를 사용하므로 g가 evaluate된다.
        • 6. toString은 denom을 사용한다. denom도 동일한 과정을 거치지만 g는 이미 한번 계산되었기 때문에 다시 계산하지는 않는다.
        • 7. result string이 출력된다.
      • 여기서 g의 순서가 numer나 denom 뒤임에도 불구하고 관련 변수들이 lazy이기 때문에 numer와 denom이 완전히 초기화되기 전에 초기화될 수 있었다.
        • 이는 textual order가 중요하지 않고 필요할 때 초기화되기 때문이다. 
        • 그러므로 lazy val은 프로그래머가 모든 게 잘돌아갈 수 있도록 val의 정의를 배열하는 것에 대해서 생각하지 않아도 되게 해준다.
      • 하지만  lazy val이 사이드 이펙트를 가지지 않고 이에 의존하지 않는 경우에만 이러한 이점을 가진다.
        • 사이드 이펙트가 있으면 초기화 순서가 문제가 된다. 그리고 초기화 코드의 순서를 파악하는 것이 매우 어렵다. 
      • 그래서 lazy val은 functional object의 이상적인 보완체이다. 초기화순서가 상관없고 모든 것이 결국 초기화된다면 말이다. imperative한 코드에는 대부분 적합하지 않다. 

20.6 추상 타입 

  • 여기에 나오는 예제를 보면 당연히 되주어야 할 것 같은데 안된다.
    • 이유는 parameter type이 다르기 때문이다. 
  • 어떤 사람들은 type system이 요런 경우에 대해서 불필요하게 엄격하다고 논쟁한다. 
    • 그들은 subclass의 method의 parameter를  특수화하기 위해 OK해야만 한다고 말한다.
    • 하지만 만약 클래스가 예제처럼 되게 한다면 unsafe 한 상황에 처하게 될꺼다.
    • Listing 20.9, Listing 20.10의 예제 꼭 참고 할 것.

20.7 경로에 의존하는 타입

  • 20.10의 예제를 돌려서 나온 코드에 보면 bessy.SuitableFood로 찍히는 코드가 있다.
    • 사실 정말 제대로 찍혀야 한다면 Grass가 찍혀야 하는 것이 좋다.
    • path는 object의 참조를 의미한다. 
    • path는 single일 수도 있고 farm.barn.bessy.SuitableFood와 같이 길수도 있다.
    • “path-dependent type”이란 type이 path에 의존적이란 뜻이다.
    • 일반적으로 different path는 different type이다.
  • path-dependent type은 Java에서의 inner class type을 위한 문법과 유사하지만, 큰 차이점이 있다. 
  • path-dependent type은 outer object이름을 따르지만, 반면에 inner class type은 outer class을 만든다. 


20.8 구조적 서브타이핑  

 

  • 스칼라는 structural subtyping을 지원한다.
    • 같은 member를 들고 있다면 말이다.
    • 이렇게 구조적인 subtyping을 위해서 refinement type을 사용한다.
  • 일단 nominal subtyping을 사용해보고 좀 더 유연할 것 같은 상황이 오면 structural subtyping을 사용해라.
  • 사용법은 책을 참조..

20.9 열거형 

  • Scala의 Enumeration은 언어에서 지원하는 문법이 아니라 path-dependent type을 이용한 라이브러리이다.
  • 사용법은 책 참조!

20.10 사례 연구: 통화 변환 (생략)

20.11 결론 

21장. 암시적 변환과 암시적 파라미터 

  • 일반적인 코드와 다른 사람이 만든 라이브러리는 기본적으로 차이가 있다.
    • 일반적인 코드는 원하는 대로 변경하고, 확장할수 있다.
    • 하지만 다른 사람의 라이브러리를 사용한다면 보통의 경우 그대로 사용할 수 밖에 없다.
  • 많은 언어들이 이 문제에 대한 해결책을 제공하고 있다.
    • 루비는 모듈을 제공
    • 스몰토크는 패키지를 각 클래스에 추가할 수 있음
  • 이러한 것들은 매우 강력하지만 또한 위험하기도 하다. 애플리케이션 전체적으로 사용되는 클래스의 행동을 수정할수도 있기 때문이다.
  • C# 3.0은 static extension method들을 제공한다.(more local)
    • 클래스에 메소드 추가만 가능하고 필드 추가는 안되기 때문에 더 제한적이다.
      • 클래스가 새로운 인터페이스를 구현하도록 만들수가 없다.
  • 이 문제에 대한 스칼라의 대답은 implicit conversion과 parameter이다.
    • 이러한 요소들은 이미 존재하는 라이브러리들을 지루하지 않으면서도 더 재미있게 다룰 수 있도록 한다.


21.1 암시적 변환 

  • Implicit conversion의 장점
    • Implicit conversion은 주로 서로를 전혀 고려하지 않고 만든 소프트웨어들이 함께 동작해야할 때 도움이 된다. 
    • Implicit conversion은 타입 변경을 위한 많은 explicit conversion을 줄여준다.
  • 스윙을 예제 참조
    • 만약에 Swing이 스칼라로 짜여졌다면..event listener가 function type이었을 것이다.
    • 아마 function literal syntax를 썼겠지..
      • 자바는 function literal이 없기 때문에 스윙은 차선책으로 one-method interface를 구현한 inner class를 사용
      • action listener의 경우 interface가 ActionLister이다.
    • page 444에 있는 코드는 많은 boilerplate가 있다.
    • 여기서 새로운 정보라고는 println밖에 없다. 많은 다른 정보들로 인해 제대로 파악을 할수가 없다. 
      • listener가 ActionListener라는 것
      • callback method이름이 actionPerformed라는 것
      • addActinoListener가 actionListener라는 것을 받는 다는 것
    • 스칼라 식으로 함수를 argument로 쓴다면 많은 boilerplate를 사용하지 않을 수 있다


21.2 암시 규칙 

  • Implicit definition은 컴파일러가 type error를 처리하기 위해서 프로그램에 그러한 것들을 추가하는 것을 허락하는 것이다.
  • Implicit conversion 규칙
    • Marking Rule: implicit을 쓴 정의들만 사용가능하다.
      • implicit은 변수, 함수, 객체 정의 등에도 사용가능하다.
      • 이런 규칙을 사용함으로써 만약 컴파일러가 랜덤으로 함수를 사용했다면 발생했을 혼란을 막을 수 있다.
      • 컴파일러는 명시적으로 implicit이라고 선언된 정의들 중에서만 선택할 것이다.
    • Scope Rule: 추가된 implicit 변환은 단일 식별자, 관련된 소스, conversion의 target type으로 scope안에 존재해야만 한다.
      • 스칼라 컴파일러는 scope안에 있는 implicit conversion만을 고려한다.
      • implicit conversion은 single identifier로서 scope안에 있어야 한다.
      • 한가지 예외
        • 컴파일러는 source 혹은 expected target type의 companion object에서 implicit definition을 찾는다.
      • Scope rule는 modular reasoning을 하도록 돕는다.
        • 파일에서 코드를 읽을 때 다른 파일에 대해서 고려해야되는 유일한 사항은 fully qualified name을 통해서 몇시적으로 참조되었는지 imported되었는지를 확인해보는 것이다.
        • 이러한 것의 장점은 explicit하게 작성된 코드만큼 implicit으로 구성된 코드도 중요하도록 해준다.
        • 만약 implicit들이 system-wide된다면 파일을 이해하기 위해서 프로그램의 모든 파일에 있는 모든 implicit에 대해서 알아야만 한다.
    • One-at-a-time Rule: 오로지 하나의 implicit만 시도된다.
      • x + y는 convert1(convert2(x)) + y로 변환되지 않는다.
      • 그렇게 하면 컴파일 타임에 에러 있는 코드가 심하게 발생할 수 있고, 프로그래머가 작성한 것과 실제로 수행되는 프로그램과의 차이가 너무 커지게 된다.
      • 컴파일러는 이미 다른 implicit을 적용하고 있는 도중에 추가적으로 implicit conversion을 추가하지 않는다.
      • implicit 안에서 implicit parameter를 사용해서 이런 제약사항을 피해갈 수 있긴 한데..뒤의 챕터에 나온다.
    • Explicit-First Rule: 코드 타입 체크가 이미 작성되어 있다고 한다면 implicit은 시도되지 않는다.
      • 컴파일러는 이미 잘 돌아가고 있는 코드를 변환하지 않는다.
      • 이 룰에 의한 결과로서 항상 implicit filter들은 explicit한 것들로 변경될 수 있다.  => 그렇게 해서 좀 길긴해도 모호성을 줄여줄 수 있다.
      • 이건 트레이드 오프가 있으니 선택해야할 문제.
      • 반복적인 코드를 나오는 곳마다, implicit conversion들은 지루함을 줄여줄 수 있다.
      • 코드가 모호해지는 단계까지 간결해지는 것처럼 보이면, explicit하게 conversion을 추가할 수 있다.
      • implicit의 양은 전적으로 style의 문제다.
  • implicit conversion의 이름 짓기
    • 하나를 콕 집어서 선택하고 싶을 때 필요하다.
    • implicit conversion은 임의의 이름을 가질 수 있다.
    • implicit conversion이 이름은 두가지 경우에만 중요하다.
      • 메소드 애플리케이션에서 explicit하게 사용하고 싶을 때
      • 프로그램에서 어떤 implicit conversion을 쓸 건지 결정하기 위해
    • page 448의 예제에서 보면 특정 implicit만을 import하기 위해서 사용한다.
  • implicit이 시도되는 곳은?
    • expected type으로의 변환 (21.3)
      • String이 있는데..이걸 IndexedSeq[Char]을 받는 함수에 넘기고 싶다고 해보자..이런 경우 implicit을 탄다.
    • - selection의 receiver로 변환(21.4)
      • ”abc”.exists가 stringWrapper(“abc”).exists로 변환된다. 왜냐하면 exists method가 Strings에서는 사용할 수 없지만 IndexedSeq에서는 사용가능하기 때문이다.
    • - implicit parameter(21.5)
      • caller가 원하는 것에 대해 호출되는 함수에게 더 많은 정보를 제공하기 위해서 사용됨
      • 보통 generic function에 유용하다. 호출되는 함수는 하나 혹은 그 이상의 argument의 타입에 대해서 전혀 몰라도 된다.


21.3 예상 타입으로의 암시적 변환 

  • Implicit conversion to an expected type은 컴파일러가 implicit을 사용하는 first place이다.
    • 한쪽 타입(X)을 보고 다른 타입(Y)이 필요하면 X => Y를 처리하는 implicit function을 찾는다.
  • Double에서 Int로 변환하는 건 정확도에 있어서 문제가 있기 때문에 우리가 추천하는 변환은 아니다.=> 당연함..
  • 머 그래도 스칼라에서 Int가 자동으로 Double로 바뀌는 건 다 scala.Predef object에 implicit들이 선언되어 있기 때문이다
    • 스칼라에서는 type 시스템이 이를 처리하는 것이 아니다. 그냥 implicit conversion으로 처리한다. 


21.4 호출 대상 객체 변환 

  •  Implicit conversion은 메소드 호출을 받는 쪽에서도 적용될 수 있다. 이런 종류의 implicit conversion은 두가지 주요한 사용처가 있다.
    • 첫번째로 receiver converions들은 새로운 클래스가 이미 존재하는 클래스 구조에 더 자연스럽게 통합되도록 해준다.
    • 두번째로 요놈들은 언어에서 DSL들을 작성할 수 있도록 해준다.
  • Interoperating with new types
    • 어떤 객체나 클래스 등에 타입변환에 대한 기능이 없으면 자동으로 추가해준다.
    • 예제를 보면 oneHalf + 1은 가능한데…1 + oneHalf는 가능하지 않다.
    • 이걸 가능하게 하기 위해서..implicit을 정의해서  1 + oneHalf가 동작하게 한다.
      • 컴파일러는 1 + oneHalf를 확인하기 위해서 체크한다.
      • 그런데 Int가 +메소드가 여러개가 있지만 Rational을 argument로 받는 건 없다는 것을 확인한다.
      • 다음으로 컴파일러는 Int에서 다른 타입으로 변환하는 implicit conversion을 찾는다. 
      • 찾아서 적용한다.
      • intToRational(1) + oneHalf
  • Simulating new syntax
    • 새로운 문법을 추가할 수 있게 해준다.
    • Map(1 -> “one”, …)
    • Map에서 ->는 언어 문법이 아니다.
      • ->는 ArrowAssoc이라는 클래스의 메소드이고..ArrowAssoc은 standard Scala preamble에 정의되어 있다.
    • preamble은 Any에서 ArrowAssoc으로의 implicit conversion도 포함하고 있다.
    • 그래서 1 -> “One”을 쓰면 -> method를 찾을 수 있도록 컴파일러는 any2ArraowAssoc을 추가한다.
    • 이 “rich wrappers” 패턴은 syntax-like 확장을 제공하는 라이브러리들에서는 일반적인 것이다.
    • receiver class에 없는 호출 메서드가 있다면 그건 아마도 implicit을 사용한 것일 거다..
    • 요거 잘 쓰면 내부 DSL을 정의할 수 있다. 

 

21.5 암시적 파라미터 

  • implicit은  argument list에도 가능하다.
  • 컴파일러는 때때로 someCall(a)를 somCall(a)(b), new SomeClass(a)를 new SomeClass(a)(b)로 변환한다.
    • 이렇게 함으로써 빠진 parameter list를 채워서 정상적으로 함수를 호출한다.
    • 예를 들어 만약에 someCall이 마지막 파라미터 리스트를 세개 가지는 데 이를 안쓴다면..컴파일러가 someCall(a)를 someCall(a)(b,c,d)로 변환한다. => 이렇게 할라믄 마지막 파라미터 리스트(b,c,d)에 impilcit이 정의되어 있어야 한다.
  • 여기서 봐야할 건 단순히 마지막 파라미터가 아니라 마지막 curried parameter 전부이다. 
    • 예를 들어 someCall(a)(b,c,d)긴데 someCall(a)로 호출하려고 한다면 b,c,d쪽에 implicit을 붙여야 한다.
  • Listing 21.1
    • implicit keyword가 각각의 파라미터가 아니라 전체 파라미터 리스트에 적용되어 있다는 것을 주목한다.
    • Listing 21.1처럼 두개의 파라미터를 받는다고 할지라도..implicit은 한번만 붙인다..
    • 이 예제에서 주의해야할 하나는 prompt와 drink의 타입으로써 String을 사용하지 않았다는 것이다.  
      • 왜냐하면 컴파일러는 scope안의 타입에 대해서 파라미터의 타입을 매칭하기 위해 parameter를 implicit하게 select하기 때문에, implicit parameter는 보통 충분히 “드물거나”, “특별한” 타입을 가진다.
      • 결론적으로 위의 예제에서 PreferredPrompt, PreferredDrink는 스코프 내에서 굉장히 드물 것이다.
    • 또 implicit parameter에 대해서 알아야 할 것은 implicit parameter들이 대부분 이전 파라미터 리스트에서 explicit하게 언급된 타입에 대한 정보를 제공하기 위해 사용된다는 점이다.(Haskell의 type class처럼)
  • Listing 21.2의 단점
    • element type이 Ordered의 서브타입이 아닌 것을 이미 가지고 있는 list에는 함수를 사용할 수 없다.
      • 예를 들어 integer의 리스트의 최대값을 찾기 위한 함수를 사용할 수 없다. 왜냐하면 Int가 Ordered[Int] subtype이 아니기 때문이다. => 19.11(page 407)에서는 Person이 Ordered[Persion]을 상속했기 때문에 사용가능..하지만 스칼라 기본 제공 type은 Ordered[Int]의 subtype일 리가 없으므로..안된다.
  • 이를 위해 maxListUpBound를 더 일반적으로 만들기 위해서 List[T] argument에 추가로 분리된 두번째 argument를 만드는 것이다.
  • 21.3에서의 예제는 이전의 parameter list에서 explicit하게 선언된 type에 대해 더 많은 정보를 제공하기 위해 implicit parameter를 사용하는 예이다.
    • 정확히 말해 implicit paramter orderer는 type T에 대한 더 많은 정보를 제공한다. 이 경우 어떻게 T를 order할 것인가에 대한 정보를 준다. 
    • Type T는 이전 parameter List에서 언급되었다. elements가 항상 maxListImpParm의 호출때마다 explicit하게 제공되어야하기 때문에 컴파일러는 T를 컴파일 시간에 알것이고 그래서 T => Ordered[T]의 implicit 정의가 scope안에 있는 지를 확인할 수 있다. 그래서 두번째 파라미터 리스트를 implicit하게 넘겨줄 수 있다.
  • 이 패턴은 standard scala library가 많은 common type에 대한 implicit “orderer”를 제공하기 때문에 일반적이다. => 그러므로 maxListImpParm 메소드를 다양한 타입에 쓸 수 있다. 
  • A style rule for implicit parameters
    • style rule로서는, implicit parameter의 타입에 custom named type을 사용하는 것이 최선이다.
    • page 459에 있는 버전의 경우 호출하는 쪽은 (T,  T) => Boolean의 orderer paramger를 제공해야만 한다. 
    • (T, T) => Boolean은..먼지 정확히 이해하기 어렵다. equality test인지..먼지..전혀..
    • 이에 비해 Listing 21.3에서 쓴..Ordered는 정확히 implicit paramter가 멀 위해서 사용되는지를 알려준다.
    • 그래서 스타일 룰에 따라: implicit paramter의 타입에서는 최소한 하나의 role-determining name을 사용하자. 


21.6 뷰 바운드 (체크!:발표자가 잘 모름;;)

  • 이전 예제에서 implicit을 사용할 기회가 있었지만..사용하지 않았다.
  • 파라미터에 implicit을 사용할 때, 컴파일러는 implicit value를 가진 파라미터를 제공하기 위해서 노력할 뿐만 아니라, 메소드의 바디에서도 implicit 파라미터를 사용할 수 있도록 한다..
  • 그래서 method body안의 orderer의 사용이 없어질 수 있는것이다.
  • 컴파일러가 Listing 21.4의 코드를 조사할 때 type이 맞지 않는다는 것을 알게된다.
    • => 예를 들어 type T의 x는 > method를 가지고 있지 않다. 그래서 x > maxRest는 동작하지 않는다.
    • => 하지만 우리의 컴파일러는 포기하지 않는다.
    • => 일단 코드를 고칠 수 있는 implicit conversion을 찾는다. 
    • => 이 경우 orderer가 사용가능하다는 것을 알려줄 것이고, code를 orderer(x) > maxRest로 바꾼다. 
  • x > maxRest는 orderer(x) > maxRest로...maxList(rest)는 maxList(rest)(orderer)로 변환될 수 있다.이 두가지 implicit의 추가가 이루어진 후에 메소드는 완벽하게 type check될 수 있다.
  • maxList를 좀 더 살펴보면..orderer parameter는 직접적으로 쓰여진 곳이 한군데도 없다.
  • 모든 orderer는 implicit이다.
  • 놀랍게도 이러한 코딩 패턴은 매우 일반적이다.
  • implicit parameter는 conversion을 위해서만 사용되므로 implicit하게 사용될 수 있다.
  • 자..파라미터 이름은 explicit하게 전혀 사용되지 않기 때문에 무엇이어도 상관이 없다.
  • 이 패턴은 일반적이기 때문에 스칼라는 파라미터의 이름을 없애고 view bound를 사용해서 method header를 짧게 만들수 있다.
  • view bound를 이용하는 예제를 Listing 21.5에서 볼 수 있다.
  • “T <% Ordered[T]” 는 다음과 같이 말하는 것과 같다
    • T가 Ordered[T]로서 사용될 수 있는 한 어떤 T든 사용할 수 있다"
      • 이는 "T가 Ordered[T]이다(표현한다면 T <: Ordered[T])"라고 하는 것과는 다르다. 
      • 예를 들어 Int가 Ordered[Int]의 subtype은 아님에도 불구하고 Int에서 Ordered[Int]로 implicit conversion이 가능한 이상 List[Int]를 maxList로 보낼 수 있다. 
      • 그러므로 만약 type T가 Ordered[T]가 될 수 있다면, 여전히 List[T]는 maxList로 보낼 수 있다. 
      • 컴파일러는 implicit identity function을 사용할 것이다.
      • 이 경우 conversion은 no-op이다. 그냥 object를 리턴할 것이다. 



21.7 여러 변환을 사용하는 경우 

  • scope 내에 multiple implicit converion이 발생할 수도 있다.
    • 대부분의 경우 Scala는 이와 같은 경우에 conversion을 추가하는 것을 거절한다.
    • Implicit은 conversion이 빠져도 되는 것이 명백하고 순수한 boilerplate인 경우에만 잘 동작한다.
  • 만약 multiple conversion이 적용된다면 선택이 전혀 명백하지 않게 된다.
  • scala 2.7까지는 multiple implicit conversion이 적용되면 컴파일러가 고르는 것을 거절했다. 
    • 이 상황은 method overloading과 같다.
      • 만약 foo(null)을 호출하려고 할 때 null을 받는 두개의 다른 foo overload가 있다면 컴파일러가 거절할 것이다.
        • 어떤 메소드 호출를 호출해야할 지 모호하다고 할 것이다.
  • scala 2.8에서는 rule이 좀 느슨해진다. 만약 유효한 conversion 중 하나가 다른 것보다 더 specific하다면 컴파일러는 더 specific한 것을 선택한다 
    • 아이디어는 프로그래머가 항상 conversion 중에 하나를 선택한다는 것을 믿기 때문에,프로그래머가 explicit하게 하지 않도록 하는 것이다.
      • 이전예제를 들어보면 만약 사용가능한 foo method 중 하나는 String을 가지고..나머지 하나는 Any를 가진다면 String을 가진녀석을 선택한다.
    • 더 간단하게 말하자면 하나의 implicit conversion은 다음의 조건을 만족하는 것보다 더 specific하다.
      • 전자의 argument type이 후자의 subtype이다. => subtype이면 specific
      • conversion들이 모두 method이고 전자의 enclosing class가 후자의 enclosing class를 extend하고 있다면.. => method가 있는데..method를 포함하고 있는 class가 extends된 녀석이라면 specific
    • 이 이슈에 대한 동기는 java collection, Scala collection, string간의 interoperation을 개선시키기 위해서 였다.


21.8 암시 디버깅 

  • Implicit은 Scala의 강력한 기능이지만 때때로 제대로 쓰기 좀 어려울 수 있다.
  • 왠지 느낌적으로는 implicit이 적용되어야 할 것 같은데..컴파일러가 왜 implicit conversion을 찾지 못하는지 궁금할 수 있다.
    • 이 경우 conversion을 explicit하게 쓰는 것이 도움이 된다. => 에러 메시지 준다.
      • error 메시지가 있다면 컴파일러가 왜 implicit을 적용하지 못하는지 알수 있다. 
    • 예를 들어 String에서 IndexedSeq가 아닌 List로 conversion을 잘못했다고 쳐보자..
      • 예제를 보면 명시적으로 wrapString을 써서 명확하게 에러를 확인할 수 있는 것을 볼 수 있다.
      • conversion을 explicit하게 추가하는 것은 error를 없앨 수도 있다. 
      • 이 예제의 경우에는 다른 룰중에 하나가 implicit conversion을 막는 것으로 볼 수 있다.
  • 프로그램을 디버깅할 때 compiler가 추가한 implicit conversion을 보는 것이 도움이 될 때가 있다.
    • argument로 -Xprint:typer를 넣어주면 유용하다.
    • 용감하다면 interactive shell 실행 시에도 넣어줄 수 있으나..이경우 많은 boilerplate를 보게 될 꺼다. 


21.9 결론 

  • No labels