: 자기의 코드는 마음대로 바꾸거나 확장 가능하지만 다른 사람의 라이브러리를 사용할 때는 보통 있는 그대로 사용한다.

=> 루비나 스몰토크에서는  다른 라이브러리를 추가해서 클래스의 동작을 원하는 대로 변경할 수 있도록 지원하지만 위험

=> C#은 지역적으로만 변경할 수 있다. 라이브러리에 메소드 추가는 가능하지만 필드 추가 X, 인터페이스 구현 X 여서 제약이 크다.

 

: 스칼라는 같은 문제에 대해 암시적 변환과 암시적 파라미터를 답으로 제공한다.

 

21.1 암시적 변환

: 암시적 변환은 서로를 고려하지 않고 독립적으로 개발된 두 덩어리의 소프트웨어를 한데 묶을 때 유용(동일한 어떤 대상을 각 라이브러리가 다르게 인코딩할 수 있다.)

: 암시적 변환을 사용하면 한 타입을 다른 타입으로 변경하는 데 필요한 명시적인 변환의 숫자를 줄일 수 있다.

 

Ex. 자바의 스윙 라이브러리

val button = new JButton
button.addActionListener(
     new ActionListener {             // 자바는 함수 리터럴이 없기 때문에 메소드가 하나뿐인 인터페이스를 구현하는 내부 클래스 사용
           def actionPerformed(event: ActionEvent) {
                  println("pressed!")
            }
     }
)

: 리스너가 ActionListener라는 사실, 콜백 메소드가 actionPerformed라는 사실, 리스너 추가 함수에 전달할 인자가 ActionEvent라는 사실 등은 addActionListener에 전달할 인자라면 당연한 것들이다. 새로운 정보는 실행할 코드 부분, 즉 println을 호출하는 부분뿐이다. => 중요한 정보가 있는 코드가 필요없는 코드를 압도

 

button.addActionListener(     // 함수를 인자로 받는 스칼라에 친화적인 버전 => 필요 없는 코드를 줄일 수 있다.
    (_: ActionEvent) => println("pressed!")
)

: addActionListener 메소드는 ActionListener을 원하기 때문에 타입 불일치 에러 => 암시적 변환 사용

 

" 함수를 ActionListener로 바꾸는 암시적 변환 코드 " : f 함수를 받아서 ActionListener를 반환

implicit def function2ActionListener(f: ActionEvent => Unit) =
     new ActionListener {
            def actionPerformed(event: ActionEvent) = f(event)

}

 

button.addActionListener(
       function2ActionListener(
                  (_: ActionEvent) => println("pressed!")
        )
)

 

button.addActionListener(           // 컴파일러가 자동으로 암시적 변환 코드를 추가해준다.
     (_: ActionEvent) => println("pressed!")
)

: 컴파일러는 코드를 있는 그대로 컴파일 => 타입 오류 발생 => 컴파일을 포기하기 전에 암시적 변환을 통해 문제를 해결할 수 있는 지 검토 => 암시적 변환을 통해 타입 오류가 사라진다면 해당 타입으로 다음 단계 진행 => 불필요한 코드를 제거할 수 있어 코드가 더 명확

 

21.2 암시 규칙

- 암시적 정의 : 컴파일러가 타입 오류를 고치기 위해 삽입할 수 있는 정의

 

< 표시 규칙 : implicit로 표시한 정의만 검토 대상이다. >

: 컴파일러는 암시적이라고 명시한 정의 중에서만 변환 함수를 찾는다.

: implicit 정의는 변수, 함수, 객체 정의에 사용 가능

: implicit 표시를 통해 컴파일러가 스코프 안에서 임의의 함수를 선택해 변환 함수로 사용하는 것 방지

 

< 스코프 규칙 : 삽입된 implicit 변환은 스코프 내에 단일 식별자로만 존재하거나, 변환의 결과나 원래 타입과 연관이 있어야 한다. >

: 암시적 변환은 단일 식별자로 스코프 안에 존재해야만 한다.

: 컴파일러는 someVariable.convert 같은 형태로 변환을 삽입하지 않는다.(x+y를 someVariable.convert(x) + y 로 확장 X )

: someVariable.convert를 사용하게 만들고 싶다면, 임포트해서 단일 식별자로 가리킬 수 있어야 한다.

Ex. 유용한 암시적 변환이 들어있는 Preamble 객체를 사용하는 코드에서는 import Preamble._을 호출해 라이브러리의 암시적 변환을 한 번에 접근할 수 있다.

: 단일 식별자 규칙의 한 가지 예외는 컴파일러는 원 타입이나 변환 결과 타입의 동반 객체에 있는 암시적 정의도 살펴본다.

Ex. Dollar 객체를 Euro를 취하는 메소드에 전달한다면 Dollar가 원 타입이고 Euro가 결과 타입 => Dollar나 Euro의 동반 객체 안에 Dollar에서 Euro로 변환하는 암시적 변환을 넣을 수 있다.

object Dollar {
    implicit def dollarToEuro(x: Dollar): Euro = ...         // dollarToEuro가 Dollar 타입과 연관이 있다.
}

: 스코프 규칙이 없다면 암시적 정의가 시스템 전체에 영향을 미치게 되고 어떤 파일의 동작을 이해하기 위해 프로그램에 있는 모든 암시적 정의를 알아야만 한다.

 

< 한 번에 하나만 규칙: 오직 하나의 암시적 선언만 사용 한다. >

: 컴파일러는 x + y 를 convert1(convert2(x)) + y 로 변환하지 않는다.

: 그렇게 하면 잘못 작성한 코드를 컴파일하는 시간이 극적으로 늘어나고 프로그래머가 작성한 코드와 그 프로그램이 실제 하는 일 사이의 차이가 커진다.

: 암시 선언 안에서 암시 파라미터를 사용해 이런 제약 우회 가능(나중에)

 

< 명시적 우선 규칙: 코드가 그 상태 그대로 타입 검사를 통과한다면 암시를 통한 변환을 시도하지 않는다. >

: 원한다면 언제든 명시적인 선언을 사용해 암시적 식별자를 대신할 수 있다. => 코드는 길어지지만 모호함은 줄일 수 있다.

: 코드가 반복이 잦고 장황하다면 암시적 변환을 사용해 지루함을 줄일 수 있다.

: 코드가 너무 간결해서 모호성이 큰 경우에는 명시적으로 변환을 추가할 수 있다.

 

< 암시적 변환 이름 붙이기 >

: 암시적 변환에는 아무 이름이나 가능

: 이름을 사용할 때는 암시적 변환을 직접 사용해 명시적으로 변환을 사용하고 싶은 경우, 프로그램의 특정 지점에서 사용 가능한 암시적 변환이 어떤 것이 있는지 파악해야 하는 경우가 있다.

object MyConversions {
    implicit def stringWrapper(s: String):  IndexedSeq[Char] = ...
    implicit def intToString(x: Int): String = ...
}

=> 프로그램에서 stringWrapper 변환을 사용하고 싶지만 intToString이 정수를 자동으로 문자열로 바꾸는 것을 막고 싶다.

 

import MyConversions.stringWrapper

=> 이름을 통해서 특정 암시적 변환만 임포트하고 그 밖의 것은 제외

 

< 암시가 쓰이는 부분 >

1. 값을 컴파일러가 원하는 타입으로 변환할 때

2. 어떤 수신 객체를 변환할 때

3. 암시적 파라미터를 지정할 때

 

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

: 컴파일러가 Y 타입이 필요한 위치에 X 타입을 봤다면, X를 Y로 변환하는 암시적 함수를 찾는다.

scala> val i: Int = 3.5
      <console>:4: error: type mismatch;
      found : Double(3.5)
      required: Int
      val i: Int = 3.5
      ^

 

scala> implicit def doubleToInt(x: Double) = x.toInt
doubleToInt: (x: Double)Int

scala> val i: Int = 3.5
i: Int = 3

: 컴파일러가 Int가 필요한 곳에서 Double인 3.5를 봄 => Double을 Int로 바꾸는 암시적 변환이 없는지 찾아본다. => doubleToInt를 찾음 ( doubleToInt는 스코프 안에 단일 식별자로 있기 때문 ) => 컴파일러는 자동으로 doubleToInt를 추가 => 코드는 val i : Int = doubleToInt(3.5)로 바뀐다.

 

: Double을 Int로 변환하는 것은 정밀도를 잃어버리기 때문에 권장 X => 제약이 많은 타입에서 더 일반적인 타입으로 변환이 이뤄지는 것이 더 타당하다. => Int에서 Double로의 변환은 이치에 맞다. 실제로 scala.Predef는 모든 스칼라 프로그램에 암시적으로 임포트 되는데 그 안에는 더 작은 수 타입을 더 큰 수 타입으로 변환하는 암시적 변환이 있다.

implicit def int2double(x: Int): Double = x.toDouble   

=> Int 값을 Double 변수에 저장할 수 있다.

 

 21.4 호출 대상 객체 변환

: 암시적 변환을 메소드를 호출하는 대상이 되는 객체인 수신 객체에 적용 가능

1. 수신 객체 변환을 통해 새 클래스를 기존 클래스 계층 구조에 매끄럽게 통합할 수 있다.

2. 기존 DSL을 통해 새로운 도메인 특화 언어를 만드는 일을 지원한다.

 

< 새 타입과 기존 타입 통합하기 >

=> 클라이언트 프로그래머들이 새로운 타입을 마치 기존 타입의 인스턴스처럼 사용할 수 있다.

class Rational(n: Int, d: Int) {
     ...
     def + (that: Rational): Rational = ...
     def + (that: Int): Rational = ...
}

scala> val oneHalf = new Rational(1, 2)
oneHalf: Rational = 1/2

scala> oneHalf + oneHalf
res0: Rational = 1/1

scala> oneHalf + 1
res1: Rational = 3/2

 

scala> 1 + oneHalf                    // 객체 1에 Rational 객체를 처리하는 + 메소드가 없다.
<console>:6: error: overloaded method value + with
     alternatives (Double)Double <and> ... cannot be applied
     to (Rational)
     1 + oneHalf
     ^

 

scala> implicit def intToRational(x: Int) =  new Rational(x, 1)    // Int를 Rational로 변경하는 암시적 변환 정의
intToRational: (x: Int)Rational

scala> 1 + oneHalf  
res2: Rational = 3/2

=> 1 + oneHalf 타입 오류 발생 => Int를 Rational을 받는 + 메소드를 정의한 다른 타입으로 변환할 수 있는 지 찾는다.

=> 컴파일러는 intToRational을 발견하고 intToRational(1) + oneHalf 로 적용한다.

 

< 새로운 문법 흉내 내기 >

: 암시적 변환을 통해 새 문법을 추가한 것처럼 흉내낼 수 있다.

Map(1 -> "one", 2 -> "two", 3 -> "three")

: ->는 문법이 아니고 표준 스칼라 프리엠블(scala.Predef)에 있는 ArrowAssoc 클래스의 메소드이다. 프리엠블에는 Any에서 ArrowAssoc로 보내는 암시적 변환이 있다.

package scala
object Predef {
  class ArrowAssoc[A](x: A) {
    def -> [B](y: B): Tuple2[A, B] = Tuple2(x, y)
  }
  implicit def any2ArrowAssoc[A](x: A): ArrowAssoc[A] =
    new ArrowAssoc(x)
  ...
}

: 풍부한 래퍼 패턴은 언어 문법을 확장하는 것 같은 기능을 제공하는 라이브러리에서 흔하다.

: RichInt나 RichBoolean 같은 RichSomething이라는 이름의 클래스를 본다면 그 클래스는 아마도 Something 클래스에 새 문법 같아 보이는 메소드를 더 추가할 가능성이 높다.

: 이러한 풍부한 래퍼는 라이브러리로 이미 구현한 내부 DSL을 통해 외부 DSL을 만드는 데 지원한다.

 

< 암시적 클래스 >

: 암시적 클래스는 implicit 키워드가 클래스 선언부 앞에 있는 클래스

: 컴파일러는 암시적 클래스의 생성자를 이용해 다른 타입에서 암시적 클래스로 가는 암시적 변환을 만든다.

: 풍부한 래퍼 패턴이 있는 클래스를 사용하고 싶은 경우 사용한다.

case class Rectangle(width: Int, Height: Int)

 

implicit class RectangleMaker(width: Int){
  def x(height: Int) = Rectangle(width, height)
}

=> implicit def RectangleMaker(width: Int) = new RectangleMaker(width) 암시적 변환이 자동으로 생성

 

scala> val myRectangle = 3 x 4                => 쉽게 사각형 객체를 만들 수 있다.

            myRectangle: Rectangle = Rectangle(3,4)

: Int에는 x 메소드 없다. => Int를 x를 제공하는 다른 어떤 클래스로 변환할 수 있는지 찾아본다. => RectangleMaker 변환을 찾고 컴파일러는 변환을 호출하는 코드를 자동으로 넣어준다. 

: 암시적인 클래스는 케이스 클래스일 수 없으며 암시 클래스의 생성자에는 파라미터가 1개만 있어야 한다.

 

21.5 암시적 파라미터

: 컴파일러는 someCall(a) 호출을 someCall(a)(b)로 바꾸거나, new SomeClass(a)를 new SomeClass(a)(b)로 바꿔서 함수 호출을 완성하는 데 필요한 빠진 파라미터 목록을 채워 준다. ( 마지막 파라미터 하나만이 아니고 커링한 마지막 파라미터 목록 전체를 채워 넣는다.)

: someCall(a) => someCall(a)(b,c,d) :  b,c,d에 대한 정의와 파라미터 목록에 implicit 표시

 

class PreferredPrompt(val preference: String)
class PreferredDrink(val preference: String)

object Greeter {
  def greet(name: String)(implicit prompt: PreferredPrompt,      // 마지막 파라미터 목록에 implicit 명시
                          drink: PreferredDrink) {

    println("Welcome, "+ name +". The system is ready.")
    print("But while you work, ")
    println("why not enjoy a cup of "+ drink.preference +"?")
    println(prompt.preference)
  }
}

 

object JoesPrefs {
  implicit val prompt = new PreferredPrompt("Yes, master> ")     // 암시적 파라미터에 사용할 정의에 implicit 명시
  implicit val drink = new PreferredDrink("tea")
}

 


scala> Greeter.greet("Joe")        // 암시적 파라미터가 스코프 안에서 단일 식별자로 존재 X
:14: error: could not find implicit value for
  parameter prompt: PreferredPrompt
  Greeter.greet("Joe")
  ^

 

scala> import JoesPrefs._     // import를 통해 스코프로 가져온다.
import JoesPrefs._

 

scala> Greeter.greet("Joe")(prompt, drink)     // 명시적으로 파라미터 전달 가능
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
  Yes, master>

scala> Greeter.greet("Joe")                 // 단일 식별자에 있기 때문에  마지막 파라미터를 컴파일러에서 대신 채워줌
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
  Yes, master> 

: prompt, drink를 String 타입으로 만들 지 않음 => 컴파일러가 암시적 파라미터를 고를 때 스코프 안에 있는 값의 타입과 파라미터 타입을 일치시키기 때문에 실수로 일치하는 일이 적도록 한다. => 암시적 파라미터 타입은 충분히 드물거나, 특별한 타입으로 만든다.

 

: 암시적 파라미터는 파라미터 목록의 앞쪽에 명시적으로 들어가야 하는 인자 타입에 대한 정보를 제공하고 싶을 경우 많이 사용

" elements를 ordering에 의거해 최댓값 반환"

def maxListOrdering[T](elements: List[T])(ordering: Ordering[T]): T =
  elements match {
    case List() =>
      throw new IllegalArgumentException("empty list!")
    case List(x) => x
    case x :: rest =>
      val maxRest = maxListOrdering(rest)(ordering)
      if (ordering.gt(x, maxRest)) x 
      else maxRest
  }

: 내부에 순서를 내장하지 않은 타입에 대해서도 위 함수를 사용 가능하고 기존 타입의 순서도 마음대로 바꿀 수 있다. => 이전에 보았던 orderedMergeSort(Ordered 트레이트를 믹스인한 타입만 가능)보다 일반적이지만 타입에 대해 매번 순서를 지정해야 돼 불편하다. => 암시적 파라미터 사용

def maxListOrdering[T](elements: List[T])(implicit ordering: Ordering[T]): T =
  elements match {
    case List() =>
      throw new IllegalArgumentException("empty list!")
    case List(x) => x
    case x :: rest =>
      val maxRest = maxListOrdering(rest)(ordering)
      if (ordering.gt(x, maxRest)) x
      else maxRest
  }

: 암시적 파라미터는 T에 대한 추가 정보 제공

: 컴파일러는 elements를 통해 T 타입 획득 => Ordering[T]에 대한 암시적 정의가 스코프 안에 있는 지 확인

: 위 메소드와 같은 패턴이 흔해서 표준 스칼라 라이브러리에는 흔한 타입에 대해 암시적인 ordering 메소드를 정의

 

scala> maxListImpParm(List(1,5,10,3))
res9: Int = 10

scala> maxListImpParm(List(1.5, 5.2, 10.7, 3.14159))
res10: Double = 10.7

scala> maxListImpParm(List("one", "two", "three"))
res11: java.lang.String = two

 

< 암시 파라미터에 대한 스타일 규칙 >

: 암시적 파라미터 타입에는 일반적이지 않은 특별한 이름의 타입을 사용한다.

def maxListPoorStyle[T](elements: List[T])(implicit orderer: (T, T) => Boolean): T

: 명시적으로 타입에 대한 어떠한 정보도 제공하지 않으며 흔한 타입이기 때문에 컴파일러에서 실수로 이상한 값을 암시적 파라미터로 넣을 수 있다. => 암시적 파라미터의 타입 안에서 역할을 알려주는 이름을 최소한 하나 이상 사용해야 한다.

21.6 맥락 바운드

: 암시적 파라미터를 사용하는 메소드 안에서도 암시적 파라미터를 생략 가능하다.


def maxListOrdering[T](elements: List[T])(implicit ordering: Ordering[T]): T =
  elements match {
    case List() =>
      throw new IllegalArgumentException("empty list!")
    case List(x) => x
    case x :: rest =>
      val maxRest = maxListOrdering(rest)        // ordering을 암시적으로 사용, 컴파일러는 스코프 안에서 Ordering[T] 타입을 찾는다
      if (ordering.gt(x, maxRest)) x
      else maxRest
  }

: 표준 라이브러리에 있는 "def implicitly[T](implicit t: T) = t"를 사용하면 T의 암시적 정의를 리턴해준다.

def maxListOrdering[T](elements: List[T])(implicit ordering: Ordering[T]): T =
  elements match {
    case List() =>
      throw new IllegalArgumentException("empty list!")
    case List(x) => x
    case x :: rest =>
      val maxRest = maxListOrdering(rest)       
      if (implicitly[Ordering[T]].gt(x, maxRest)) x     // Ordering[T] 타입의 암시적 정의 리턴
      else maxRest
  }

: 암시적 파라미터 이름인 ordering이 전혀 사용 안 되었다. => 맥락 바운드를 사용해 파라미터 이름을 없앨 수 있다.

: 맥락 바운드는 일반적인 타입 파라미터 T를 소개하고 암시적 파라미터를 추가한다.

def maxListOrdering[T : Ordering](elements: List[T]): T =
  elements match {
    case List() =>
      throw new IllegalArgumentException("empty list!")
    case List(x) => x
    case x :: rest =>
      val maxRest = maxListOrdering(rest)
      if (implicitly[Ordering[T]].gt(x, maxRest)) x
      else maxRest
  }

: [T <: Ordered[T]] 라고 쓰면 T가 Ordered[T] 타입이어야 한다. 반면 [T : Ordering]은 T와 관련 있는 정보가 존재해야 한다는 의미이다. => 맥락 바운드는 타입의 정의를 변경하지 않고도 타입에 대한 정보를 추가할 수 있다.

 

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

scala> def printLength(seq: Seq[Int]) = println(seq.length)
printLength: (seq: Seq[Int])Unit

scala> implicit def intToRange(i: Int) = 1 to i
intToRange: (i: Int)scala.collection.immutable.Range.Inclusive
with scala.collection.immutable.Range.ByOne

scala> implicit def intToDigits(i: Int) =
  |   i.toString.toList.map(_.toInt)
intToDigits: (i: Int)List[Int]

scala> printLength(12)
:21: error: type mismatch;
  found   : Int(12)
  required: Seq[Int]
  Note that implicit conversions are not applicable because
  they are ambiguous:
  ...

: Range와 List 값 모두 다형성 때문에 Seq 타입 변수에 저장 가능 => 컴파일러에서 어떤 암시적 변환을 수행할 지 애매하기 때문에 프로그래머가 어떤 것을 원하는지 명시해야만 한다. ( 스칼라 2.7 버전까지 )

 * 메소드 오버로드 - foo(null)을 호출했는 데 foo를 오버로드한 것 중 null을 받을 수 있는 경우가 둘 이상이면 컴파일 오류

: 2.8 버전부터 가능한 변환 중 하나가 다른 하나보다 절대적으로 더 구체적이라면 컴파일러는 더 구체적인 것을 선택한다.

 * 메소드 오버로드 - 사용 가능한 foo 메소드가 하나는 String을 다른 하나는 Any를 받는다면 최종적으로 구체적인 메소드 String 선택

  * 클래스의 경우 서브 타입일수록, 메소드의 경우 둘러싼 클래스가 서브 타입일수록 구체적

 

: 규칙을 변경한 동기는 자바 컬렉션과 스칼라 컬렉션, 문자열의 상호 작용성을 향상하기 위해서

val cba = "abc".reverse

: 스칼라 2.7에서는 "abc"를 스칼라 컬렉션으로 변환 => reverse는 컬렉션 반환 => cba는 컬렉션

=> "abc == "abc".reverse.reverse 는 false

: 2.8부터는 string에서 StringOps라는새로운 타입으로 변환하는 구체적인 암시적 변환이 생겨 StringOps에서 reverse가 String 반환

   * StringOps로 변환하는 기능은 Predef에 들어있지만 스칼라 컬렉션으로 변환하는 것은 LowPriorityImplicits에 있다.

   * StringOps로 변환하는 쪽이 LowPriorityImplicts 클래스의 서브 클래스에 있기 때문에 컴파일러는 StringOps를 선택

 

21.8 암시 디버깅

: 암시를 컴파일러가 찾지 못하는 경우 변환을 명시적으로 써본다. 그럴 때 오류가 발생하면 컴파일러가 왜 암시를 못 찾는 지 알 수 있다.

" wrapString을 String에서 IndexedSeq가 아니라 List로 변환하도록 만든 경우

scala> val chars: List[Char] = "xyz"
:19: error: type mismatch;
  found   : java.lang.String("xyz")
  required: List[Char]
  val chars: List[Char] = "xyz"
  ^

 

=> wrapString 변환을 명시적으로 써 잘못이 어디 있는 지 찾을 수 있다.
scala> val chars: List[Char] = wrapString("xyz")
:19: error: type mismatch;
  found   : scala.collection.immutable.WrappedString
  required: List[Char]
  val chars: List[Char] = wrapString("xyz")
  ^

: 명시적으로 써도 못 찾는다면 scalac에 -Xpring:typer 옵션을 주면 컴파일러는 타입 검사기가 추가한 모든 암시적 변환이 있는 코드를 보여준다.

Ex.  Mocha.this.enjoy("reader")  => Mocha.this.enjoy("reader")(Mocha.this.pref)

+ Recent posts