-
[스칼라] 암시적 변환과 암시적 파라미터 (implicit)스칼라 2022. 2. 26. 16:42
Programming in scala 4th edition
1. 암시적 변환
스칼라에서는 맞지않는 타입때문에 컴파일에러가 발생했을때 암시적인 변환규칙이 있는지를 찾아본다.
import java.awt.event.{ActionEvent, ActionListener} import javax.swing.JButton val button = new JButton() button.addActionListener( new ActionListener { def actionPerformed(event: ActionEvent) = { println("pressed") } } )
자바 스윙에서 버튼을 추가할때 버튼을 눌렀을때의 동작을 정의하는 방법은 위와 같다. addListener를 이용해서 ActionListener를 구체화한 인스턴스를 전달해주어야한다. 암시적 변환을 활용하면 위의 코드를 아래와 같이 간단하게 할 수 있다.
button.addActionListener( (_: ActionEvent) => println("pressed") )
addActionListener는 actionPerformed가 정의된 ActionListener 인스턴스를 받고 싶은데 주어진것은 (_: ActionEvent) => Unit 형태의 함수이다. 여기서 적절하게 (_: ActionEvent) => Unit 형태의 함수를 ActionListener 타입의 인스턴스로 변환하는 암시적 변환을 추가해 주면 위와 같이 간단하게 사용할 수 있다.
implicit def function2ActionListener(f: ActionEvent => Unit) = new ActionListener { def actionPerfromed(event: ActionEvent) = f(event) }
컴파일러가 ActionListener가 필요한 부분에 (ActionEvent => Unit) 형태가 주어졌다는 에러를 발견한 경우 위의 암시적 변환을 적용해본다.
2. 암시 규칙
컴파일러가 암시적변환을 사용할 때는 세 가지이다.
- 값을 컴파일러가 원하는 타입으로 변환 (메서드를 호출했을 때 타입 에러 발생시 암시적 호출로 변환해봄)
- 어떤 선택의 수신 객체를 변환 (어떤 클래스에서 지원하지 않는 메서드를 호출했을때, 그 클래스에 적용할 수 있는 암시적 변환을 이용하여 메서드를 호출 할 수 있는지를 고려)
- 암시적 파라미터
컴파일러가 암시적 변환을 적용할 때 사용하는 규칙은 다음과 같다.
표시 규칙: implicit으로 표시한 정의만 검토 대상
컴파일러는 암시적 변환을 적용할때 implicit이 명시된 함수만 찾아본다.
스코프 규칙: implicit 변환은 스코프 내 단일 식별자로만 존재해야함 또는 변환의 결과나 원래 타입과 연관이 있어야 함
컴파일러는 단일식별자로 존재하며 스코프 안에 있는 암시적 변환만을 고려한다. x + y에 어떤 암시적 변환 someVariable.convert(x) + y를 적용할 수는 없다. 다른 스코프에 있는 암시적 변환을 적용하고 싶으면 import module._ 같은 형태로 임포트 해야한다.
한가지 예외는 원래 타입이나 변환 결과 타입 정의 내에 암시적 변환이 있으면 그 암시적 변환은 적용된다.
한번에 하나만 규칙: 하나의 암시적 선언만 사용
컴파일러는 x + y에 대해서 convert2(convert1(x)) + y의 형태로 변환하지 않는다.
명시성 우선 규칙: 타입 검사를 문제 없이 통과하는 코드에 대해서 암시적 변환을 시도하지 않음
3. 예상 타입으로의 암시적 변환
컴파일러는 A타입이 필요한 부분에서 B타입이 사용되면 B타입을 A타입으로 변환시키는 암시를 찾아본다.
implicit def doubleToInt(x: Double): Int = { x.toInt } def NeedIntegerButDoubleGiven(input: Int): Int = { input * 10 } NeedIntegerButDoubleGiven(1.5) // Int 가 필요한데 1.5가 주어졌으므로 doubleToInt를 적용하여 1로 변경, 이후 * 10을 적용하여 10이 반환됨
4. 호출 대상 객체 변환
암시적 변환은 메서드 호출 대상이 되는 수신 객체에도 적용할 수 있다.
class A class B { def whoami() = println("I`m B") } implicit def classAToB(a: A) = { new B() } val a = new A() a.whoami // I`m B 출력됨
a 에는 whoami 가 없음에도 a.whoami를 호출 했을때 A를 B로 변환하는 암시적 변환이 적용되어 I`m B 가 출력되는 모습이다.
새 타입과 함께 통합하기
이를 활용하면 기존 타입과 새 타입을 통합시키는데도 활용할 수 있다.
class Rational(n: Int, d: Int) { val numer = n val denom = d def +(int: Int) = { new Rational(n + int * denom, denom) } override def toString: String = { s"$numer / $denom" } } new Rational(1, 2) + 1 1 + new Rational(1, 2) // Int 에 Rational과의 + 가정의되어있지 않으므로 에러 발생
위의 예제에서 1 + new Rational(1, 2)는 Int 클래스 내부에 Rational타입과 + 가 정의되어 있지 않아서 예외가 발생한다. 아래처럼 int를 Rational로 바꿔주는 암시적 변환을 정의하면 처리할 수 있다.
class Rational(n: Int, d: Int) { val numer = n val denom = d def +(int: Int) = { new Rational(n + int * denom, denom) } def +(that: Rational) = { new Rational(this.numer * that.denom + that.numer * this.denom, this.denom * that.denom) } override def toString: String = { s"$numer / $denom" } } implicit def intToRational(x: Int): Rational = { new Rational(x, 1) } new Rational(1, 2) + 1 1 + new Rational(1, 2)
1 + new Rational을 실행할 때 컴파일러는 우선 Int의 + 메서드중 Rational을 인자로 받는 메서드를 찾아본다. Rational을 인자로 받는 + 가 정의되어 있지 않기 때문에 Int를 인자로 Rational을 받을 수 있는 + 메서드를 정의한 타입으로 변환할 수 있는지 찾아본다.
새로운 문법 흉내내기
스칼라에서 -> 또한 암시적 변환을 이용한 메서드이다. Predef에 Any에서 ArrowAssoc 클래스로의 암시적 변환이 적용되어있고, ArrowAssoc내에 -> 메서드가 정의되어있다.
implicit final class ArrowAssoc[A](self : A) extends scala.AnyVal { @scala.inline def ->[B](y : B) : scala.Tuple2[A, B] = { /* compiled code */ } @scala.deprecated(message = "Use `->` instead. If you still wish to display it as one character, consider using a font with programming ligatures such as Fira Code.", since = "2.13.0") def →[B](y : B) : scala.Tuple2[A, B] = { /* compiled code */ } }
암시적 클래스
암시적 클래스는 클래스 선언 가장 앞 부분에 implicit이 붙어있는 클래스다. 암시적 클래스는 생성자를 이용해서 다른 타입에서 그 클래스로 가는 암시적 변환을 만들어둔다.
case class Rectangle(width: Int, height: Int) implicit class RectangleMaker(width: Int) { def x(height: Int) = Rectangle(width, height) } 1 x 2 // Rectangle(1, 2) 생성됨
implicit 클래스의 제약:
- 케이스 클래스 일 수 없음
- 생성자에는 파라미터가 1개만 있어야함
- 다른 객체, 클래스, 트레이트와 같은 파일에 들어있어야함
5. 암시적 파라미터
// 5 implicit parameter class PreferredPrompt(val preference: String) object Greeter { def greet(name: String)(implicit prompt: PreferredPrompt) = { println("Welcome, " + name + ". The system is ready.") println(prompt.preference) } } Greeter.greet("Park")(new PreferredPrompt("greet > ")) // Welcome, Park. The system is ready. // greet > implicit val p = new PreferredPrompt("implicit prompt") Greeter.greet("Implicit Park") // Welcome, Implicit Park. The system is ready. // implicit prompt
함수 인자에 대해서도 암시적 파라미터를 정의해서 활용할 수 있다. Greeter.greet의 prompt는 implicit 키워드를 붙여두었다. greet 함수는 name, prompt를 모두 명시적으로 주고 실행시킬수도 있고, 암시적 파라미터를 이용해서 실행 시킬 수도 있다.
암시적 파라미터는 그 타입을 구체적으로 지정해두고 사용하는게 실수를 줄이는데 도움이 된다. 만약 prompt를 String으로 설정해 두면 코드가 꼬이기 더 쉬울 것이다.
암시적 파라미터가 많이 쓰이는 경우는 명시적 인자 타입에 대한 추가 정보를 제공하는 경우이다. 아래에서 T의 Ordering이 암시적으로 정의되어있다면 굳이 명시적으로 주어지지 않아도 함수가 문제 없이 실행된다.
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 } maxListOrdering(List(1, 3, 2, 4))
6. 맥락바운드(context bound)
ordering을 암시적으로 활용하는 함수의 경우 본문 안에서도 굳이 호출할 필요가 없다.
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(ordering.gt(x, maxRest)) x else maxRest } maxListOrdering(List(1, 3, 2, 4))
위의 함수 본문 안에 maxListOrdering의 호출 또한 ordering을 명시적으로 부르지 않고 암시적으로 호출했다. 표준 라이브러리 안의 implicitly를 사용하면 if문 안의 ordering을 없앨 수도 있다.
def implicitly[T](implicit 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 else maxRest }
이제 함수의 본문 안에는 ordering이 존재하지 않는다. 스칼라는 ordering 파라미터 자체를 없앨 수 있는 문법도 제공하는데 이것이 바로 맥락 바운드 (Context bound)이다.
def maxList[T : Ordering](elements: List[T]): T = elements match { case List() => throw new IllegalArgumentException("empty list!") case List(x) => x case x :: rest => val maxRest = maxList(rest) if (implicitly[Ordering[T]].gt(x, maxRest)) x else maxRest }
[T: Ordering]은 맥락 바운드로 두 가지 기능을 한다.
- T: 타입 T를 명시함
- Ordering[T] 라는 암시적 파라미터를 추가함
맥락 바운드가 추가한 암시적 파라미터는 implicitly 를 이용하여 본문 안에서 활용할 수 있다.
'스칼라' 카테고리의 다른 글
[스칼라] 컬렉션 자세히 보기 (0) 2022.02.28 [스칼라] 리스트 구현 (0) 2022.02.27 [스칼라] 추상 멤버 (0) 2022.02.25 [스칼라] getter setter (0) 2022.02.23 [스칼라] 컬렉션 (0) 2022.02.23