에타 확장

에타 확장 (Eta-Expansion)

Scala 컬렉션 클래스의 map 메서드 Scaladoc을 보면, 이 메서드가 함수 값을 받도록 정의되어 있음을 알 수 있어요.

def map[B](f: A => B): List[B]
//            ^^^^^^ function type from `A` to `B`

실제로 Scaladoc은 "f는 각 요소에 적용할 함수입니다"라고 분명히 말해요. 그런데 그럼에도 어떻게 map메서드를 넘겨도 잘 작동할까요?

def times10(i: Int) = i * 10   // a method
List(1, 2, 3).map(times10)     // List(10,20,30)

왜 이게 가능한 걸까요? 그 뒤에 숨은 과정을 **에타 확장(eta-expansion)**이라고 불러요. 메서드 타입의 표현식을 동등한 함수 타입의 표현식으로 변환하는데, 그것을 아주 매끄럽고 조용하게 해내죠.

출처: Scala 3 Book

본문

메서드와 함수의 차이

메서드와 함수의 핵심 차이는, 함수는 객체라는 점이에요. 즉 어떤 클래스의 인스턴스이고, 따라서 자기 자신의 메서드를 가지고 있죠(예: 함수 f에 대해 f.apply를 시도해 보세요).

반면 메서드는 넘겨 다닐 수 있는 값이 아니에요. 메서드 적용(method application)을 통해서만 호출할 수 있죠(예: foo(arg1, arg2, ...)). 메서드를 값으로 바꾸려면, 필요한 인자가 주어졌을 때 그 메서드를 호출할 함수 값을 만들면 돼요. 이것이 바로 에타 확장입니다.

좀 더 구체적으로, 자동 에타 확장 덕분에 컴파일러는 인자가 없는 메서드 참조를, 그 메서드를 호출할 동등한 익명 함수로 자동으로 변환해요. 예를 들어 위 코드의 times10 참조는 x => times10(x)로 다시 쓰여져요.

def times10(i: Int) = i * 10
List(1, 2, 3).map(x => times10(x)) // eta expansion of `.map(times10)`

궁금한 분을 위해 덧붙이면, 에타 확장이라는 용어는 람다 대수(Lambda Calculus)에서 유래했어요.

에타 확장은 언제 일어날까?

자동 에타 확장은 문맥에 의존하는 디슈가링(desugaring)이에요. 즉 메서드 참조 주변 코드에 따라 확장이 조건부로 활성화된다는 뜻이죠.

Scala 2에서는 에타 확장이 기대 타입이 함수 타입일 때만 자동으로 일어나요. 예를 들어 다음은 실패합니다:

def isLessThan(x: Int, y: Int): Boolean = x < y

val methods = List(isLessThan)
//                 ^^^^^^^^^^
// error: missing argument list for method isLessThan
// Unapplied methods are only converted to functions when a function type is expected.
// You can make this conversion explicit by writing `isLessThan _` or `isLessThan(_,_)` instead of `isLessThan`.

이 문제를 수동 에타 확장으로 해결하는 방법은 아래에서 볼게요.

Scala 3부터는 메서드 참조를 값이 필요한 어디에서나 그대로 쓸 수 있고, 일치하는 타입의 함수 객체로 자동 변환돼요. 예를 들면:

def isLessThan(x: Int, y: Int): Boolean = x < y

val methods = List(isLessThan)       // works

수동 에타 확장

언제든 메서드를 함수 값으로 수동으로 에타 확장할 수 있어요. 몇 가지 예를 보면요:

Scala 2에서는 다음 세 가지 방법을 씁니다:

val methodsA = List(isLessThan _)               // way 1: expand all parameters
val methodsB = List(isLessThan(_, _))           // way 2: wildcard application
val methodsC = List((x, y) => isLessThan(x, y)) // way 3: anonymous function

Scala 3에서는 자동 확장이 워낙 매끄러워서 이렇게 두 가지 정도면 충분해요:

val methodsA = List(isLessThan(_, _))           // way 1: wildcard application
val methodsB = List((x, y) => isLessThan(x, y)) // way 2: anonymous function

요약

이 입문서의 목적에 맞춰 기억할 중요한 점은 두 가지예요:

  • 에타 확장은 메서드를 함수처럼 쓸 수 있게 해 주는 유용한 디슈가링입니다.
  • 자동 에타 확장은 Scala 3에서 거의 완전히 매끄럽게 개선됐어요.

더 알아보기

  • 자세한 동작 원리는 Reference 문서의 Eta Expansion 페이지에서 확인할 수 있습니다.