ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스칼라] 암시적 변환과 암시적 파라미터 (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

    댓글

Designed by Tistory.