오른쪽 결합 확장 메서드: 세부 사항

오른쪽 결합 확장 메서드: 세부 사항 (Right-Associative Extension Methods: Details)

확장 메서드가 가질 수 있는 가장 일반적인 시그니처와, 이름이 :로 끝날 때 오른쪽 결합 연산자로 취급되는 과정을 자세히 살펴볼게요. 특히 컴파일러가 파라미터 순서를 어떻게 바꾸는지, 그래서 호출 시 어떤 점을 조심해야 하는지가 핵심이에요.

출처: Scala 3 Reference

본문

확장 메서드가 가질 수 있는 가장 일반적인 시그니처는 다음과 같아요.

  • 선택적인 타입 절 leftTyParams
  • 아마 비어 있는 using 절 목록 leadingUsing
  • 단일 파라미터 leftParam (명시적 용어 절 안에)
  • 아마 비어 있는 using 절 목록 trailingUsing
  • 이름 (그 앞에 def 키워드)
  • 선택적인 타입 절 rightTyParams
  • 선택적인 단일 파라미터 rightParam (명시적 용어 절 안에)
  • 임의 개수의 임의 절 rest

예를 들어 볼게요.

  extension [T]                               // <-- leftTyParams
            (using a: A, b: B)(using c: C)    // <-- leadingUsing
            (x: X)                            // <-- leftParam
            (using d: D)                      // <-- trailingUsing
    def +:: [U]                               // <-- rightTyParams
            (y: Y)                            // <-- rightParam
            (using e: E)(z: Z)                // <-- rest

확장 메서드는 이름이 :로 끝나고, 그 직후에 단일 명시적 용어 파라미터(즉, rightParam이 존재)가 이어질 때 오른쪽 결합 연산자(SLS §6.12.3 참조)로 취급돼요. 위 예제에서 그 파라미터는 (y: Y)예요.

Scala 컴파일러는 x +: xs 같은 오른쪽 결합 중위(infix) 연산을, x가 순수 표현식이거나 call-by-name 파라미터라면 xs.+:(x)로, 그렇지 않으면 val y = x; xs.+:(y)로 전처리해요. 일반적인 오른쪽 결합 중위 메서드가 오른쪽 피연산자의 클래스에 정의되기 때문에 이 처리가 필요해요. 이 스왑을 보완하기 위해 오른쪽 결합 확장 메서드의 확장(expansion)은 역(逆) 파라미터 스왑을 수행해요. 더 정확히 말하면, rightParam이 존재한다면 확장 메서드 확장의 전체 파라미터 시퀀스는 다음과 같아요.

    leftTyParams  leadingUsing  rightTyParams  rightParam  leftParam  trailingUsing  rest

즉, leftParams trailingUsingrightTyParam rightParam과 바꾼다고 볼 수 있어요.

예를 들어 위의 +:: 메서드는 이렇게 될 거예요.

  <extension> def +:: [T]
                      (using a: A, b: B)(using c: C)
                      [U]
                      (y: Y)
                      (x: X)
                      (using d: D)
                      (using e: E)(z: Z)

파라미터 사이에 의존성이 있는 오른쪽 결합 확장 메서드를 쓸 때는 이 확장을 염두에 둬야 해요.

이 확장은 또한 중위가 아닌 형태로 확장 메서드를 호출할 때도 약간의 불일치를 만들어내요. 호출 지점에서 인자 순서를 수동으로 뒤집어야 해요. 예를 들어 볼게요.

  extension [T](x: T)
    def *:(xs: List[T]): List[T] = ...

  y.*:(ys) // error when following the parameter definition order
  ys.*:(y)

  *:(y)(ys) // error when following the parameter definition order
  *:(ys)(y)

이 표현의 또 다른 한계는 def의 타입 파라미터를 명시적으로 전달하는 것이 불가능하다는 거예요(접두사 형태로 호출하지 않는 한). 예를 들어 볼게요.

  extension (x: Int)
    def *:[T](xs: List[T]): List[T] = ...

  xs.*:[Int](1) // error when trying to set T explicitly

오른쪽 결합 확장 메서드의 확장은 컨텍스트 파라미터를 명시적으로 전달할 수 있는 순서에도 영향을 줘요.

그룹 확장도 직관적이지 않게 동작할 수 있어요. 일반적으로 그룹 안의 모든 확장은 수신자(receiver)에 대한 확장이에요. 그런데 그 확장 중 하나가 오른쪽 결합 확장 메서드라면, 그 하나는 인자의 타입에 대한 확장이 돼요. 예를 들어 볼게요.

  extension (a: Int)
    def :+(b: Long): Long = ... // extension on Int
    def +:(b: Long): Long = ... // extension on Long