: 자기의 코드는 마음대로 바꾸거나 확장 가능하지만 다른 사람의 라이브러리를 사용할 때는 보통 있는 그대로 사용한다.
=> 루비나 스몰토크에서는 다른 라이브러리를 추가해서 클래스의 동작을 원하는 대로 변경할 수 있도록 지원하지만 위험
=> C#은 지역적으로만 변경할 수 있다. 라이브러리에 메소드 추가는 가능하지만 필드 추가 X, 인터페이스 구현 X 여서 제약이 크다.
: 스칼라는 같은 문제에 대해 암시적 변환과 암시적 파라미터를 답으로 제공한다.
21.1 암시적 변환
: 암시적 변환은 서로를 고려하지 않고 독립적으로 개발된 두 덩어리의 소프트웨어를 한데 묶을 때 유용(동일한 어떤 대상을 각 라이브러리가 다르게 인코딩할 수 있다.)
: 암시적 변환을 사용하면 한 타입을 다른 타입으로 변경하는 데 필요한 명시적인 변환의 숫자를 줄일 수 있다.
Ex. 자바의 스윙 라이브러리
val button = new JButton |
: 리스너가 ActionListener라는 사실, 콜백 메소드가 actionPerformed라는 사실, 리스너 추가 함수에 전달할 인자가 ActionEvent라는 사실 등은 addActionListener에 전달할 인자라면 당연한 것들이다. 새로운 정보는 실행할 코드 부분, 즉 println을 호출하는 부분뿐이다. => 중요한 정보가 있는 코드가 필요없는 코드를 압도
button.addActionListener( // 함수를 인자로 받는 스칼라에 친화적인 버전 => 필요 없는 코드를 줄일 수 있다. : addActionListener 메소드는 ActionListener을 원하기 때문에 타입 불일치 에러 => 암시적 변환 사용 |
" 함수를 ActionListener로 바꾸는 암시적 변환 코드 " : f 함수를 받아서 ActionListener를 반환 implicit def function2ActionListener(f: ActionEvent => Unit) = }
button.addActionListener(
button.addActionListener( // 컴파일러가 자동으로 암시적 변환 코드를 추가해준다. |
: 컴파일러는 코드를 있는 그대로 컴파일 => 타입 오류 발생 => 컴파일을 포기하기 전에 암시적 변환을 통해 문제를 해결할 수 있는 지 검토 => 암시적 변환을 통해 타입 오류가 사라진다면 해당 타입으로 다음 단계 진행 => 불필요한 코드를 제거할 수 있어 코드가 더 명확
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 { => 프로그램에서 stringWrapper 변환을 사용하고 싶지만 intToString이 정수를 자동으로 문자열로 바꾸는 것을 막고 싶다.
import MyConversions.stringWrapper => 이름을 통해서 특정 암시적 변환만 임포트하고 그 밖의 것은 제외 |
< 암시가 쓰이는 부분 >
1. 값을 컴파일러가 원하는 타입으로 변환할 때
2. 어떤 수신 객체를 변환할 때
3. 암시적 파라미터를 지정할 때
21.3 예상 타입으로의 암시적 변환
: 컴파일러가 Y 타입이 필요한 위치에 X 타입을 봤다면, X를 Y로 변환하는 암시적 함수를 찾는다.
scala> val i: Int = 3.5
scala> implicit def doubleToInt(x: Double) = x.toInt |
: 컴파일러가 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) { scala> val oneHalf = new Rational(1, 2)
scala> 1 + oneHalf // 객체 1에 Rational 객체를 처리하는 + 메소드가 없다. |
scala> implicit def intToRational(x: Int) = new Rational(x, 1) // Int를 Rational로 변경하는 암시적 변환 정의 => 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){ => 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)
object JoesPrefs {
scala> import JoesPrefs._ // import를 통해 스코프로 가져온다.
scala> Greeter.greet("Joe")(prompt, drink) // 명시적으로 파라미터 전달 가능 |
: prompt, drink를 String 타입으로 만들 지 않음 => 컴파일러가 암시적 파라미터를 고를 때 스코프 안에 있는 값의 타입과 파라미터 타입을 일치시키기 때문에 실수로 일치하는 일이 적도록 한다. => 암시적 파라미터 타입은 충분히 드물거나, 특별한 타입으로 만든다.
: 암시적 파라미터는 파라미터 목록의 앞쪽에 명시적으로 들어가야 하는 인자 타입에 대한 정보를 제공하고 싶을 경우 많이 사용
" elements를 ordering에 의거해 최댓값 반환" def maxListOrdering[T](elements: List[T])(ordering: Ordering[T]): T = |
: 내부에 순서를 내장하지 않은 타입에 대해서도 위 함수를 사용 가능하고 기존 타입의 순서도 마음대로 바꿀 수 있다. => 이전에 보았던 orderedMergeSort(Ordered 트레이트를 믹스인한 타입만 가능)보다 일반적이지만 타입에 대해 매번 순서를 지정해야 돼 불편하다. => 암시적 파라미터 사용
def maxListOrdering[T](elements: List[T])(implicit ordering: Ordering[T]): T = : 암시적 파라미터는 T에 대한 추가 정보 제공 : 컴파일러는 elements를 통해 T 타입 획득 => Ordering[T]에 대한 암시적 정의가 스코프 안에 있는 지 확인 : 위 메소드와 같은 패턴이 흔해서 표준 스칼라 라이브러리에는 흔한 타입에 대해 암시적인 ordering 메소드를 정의
scala> maxListImpParm(List(1,5,10,3)) |
< 암시 파라미터에 대한 스타일 규칙 >
: 암시적 파라미터 타입에는 일반적이지 않은 특별한 이름의 타입을 사용한다.
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"
=> wrapString 변환을 명시적으로 써 잘못이 어디 있는 지 찾을 수 있다. |
: 명시적으로 써도 못 찾는다면 scalac에 -Xpring:typer 옵션을 주면 컴파일러는 타입 검사기가 추가한 모든 암시적 변환이 있는 코드를 보여준다.
Ex. Mocha.this.enjoy("reader") => Mocha.this.enjoy("reader")(Mocha.this.pref)
'스칼라' 카테고리의 다른 글
스칼라 23장 for 표현식 다시 보기(Programming in Scala, 3rd) (0) | 2019.06.24 |
---|---|
스칼라 22장 리스트 구현(Programming in Scala, 3rd) (0) | 2019.06.23 |
스칼라 20장 추상 멤버(Programming in Scala, 3rd) (0) | 2019.06.07 |
스칼라 19장 타입 파라미터화, 변성(Programming in Scala, 3rd) (0) | 2019.06.07 |
스칼라 18장 변경 가능한 객체(Programming in Scala, 3rd) (0) | 2019.06.07 |