의존 함수 타입

의존 함수 타입

함수의 결과 타입이 매개변수 값에 따라 달라질 수 있다면 어떨까요? 이번 장에서는 그런 함수 타입인 **의존 함수 타입(dependent function type)**과, 그 전 단계인 의존 메서드 타입을 함께 알아볼게요. 다소 고급 주제라서, 주로 라이브러리를 직접 설계하거나 고급 라이브러리를 쓸 때 마주치게 돼요.

출처: Scala 3 Book

이 문서 페이지는 Scala 3 전용이며, Scala 2에는 없는 새 개념을 다룰 수 있어요. 별도로 언급하지 않는 한, 이 페이지의 모든 코드 예시는 Scala 3을 사용한다고 가정해요.

본문

의존 함수 타입(dependent function type) 은 함수 타입 중에서도 결과 타입이 함수의 매개변수 값에 의존할 수 있는 타입을 말해요. 의존 타입과 의존 함수 타입의 개념은 더 고급이라, 보통은 직접 라이브러리를 설계하거나 고급 라이브러리를 사용할 때만 마주치게 돼요.

의존 메서드 타입 (Dependent Method Types)

서로 다른 타입의 값을 저장할 수 있는 이종(heterogenous) 데이터베이스를 예로 생각해 볼게요. 키(key)는 그에 대응하는 값의 타입이 무엇인지에 대한 정보를 담고 있어요.

trait Key { type Value }

trait DB {
  def get(k: Key): Option[k.Value] // a dependent method
}

키가 주어지면 get 메서드가 맵에 접근해 k.Value 타입의 저장된 값을 잠재적으로 반환해 줘요. 이 경로 의존 타입(path-dependent type) 을 이렇게 읽을 수 있어요. "인자 k의 구체적인 타입에 따라, 그에 맞는 값을 반환해요."

예를 들어 이런 키들이 있다고 해볼게요.

object Name extends Key { type Value = String }
object Age extends Key { type Value = Int }

그러면 get 메서드에 대한 다음 호출들은 타입 검사를 통과해요.

val db: DB = ...
val res1: Option[String] = db.get(Name)
val res2: Option[Int] = db.get(Age)

db.get(Name)을 호출하면 Option[String] 타입의 값을 반환하고, db.get(Age)를 호출하면 Option[Int] 타입의 값을 반환해요. 반환 타입이 get에 전달되는 인자의 구체적인 타입에 의존 하죠. 그래서 이름이 의존 타입 인 거예요.

의존 함수 타입 (Dependent Function Types)

위에서 봤듯이 Scala 2에도 의존 메서드 타입은 이미 지원됐어요. 다만 DB 타입의 값을 만드는 건 꽤 번거로워요.

// a user of a DB
def user(db: DB): Unit =
  db.get(Name) ... db.get(Age)

// creating an instance of the DB and passing it to `user`
user(new DB {
  def get(k: Key): Option[k.Value] = ... // implementation of DB
})

DB의 익명 내부 클래스(anonymous inner class)를 일일이 만들어서 get 메서드를 구현해야 해요. 서로 다른 DB 인스턴스를 많이 만들어야 하는 코드에서는 엄청 지루한 일이죠.

DB 트레이트는 추상 메서드 get 하나만 가지고 있어요. 람다 문법을 쓰면 좋지 않을까요?

user { k =>
  ... // implementation of DB
}

실은 이제 Scala 3에서는 이게 가능해요! DB의존 함수 타입 으로 정의할 수 있거든요.

type DB = (k: Key) => Option[k.Value]
//        ^^^^^^^^^^^^^^^^^^^^^^^^^^^
//      A dependent function type

DB를 이렇게 정의했으니 위의 user 호출은 그대로 타입 검사를 통과해요.

의존 함수 타입의 내부 동작에 대해 더 알고 싶다면 레퍼런스 문서를 참고하세요.

사례 연구: 수치 표현식 (Case Study: Numerical Expressions)

숫자의 내부 표현에 대해 추상화하는 모듈을 정의하고 싶다고 가정해 볼게요. 예를 들어 자동 미분(automatic derivation) 라이브러리를 구현할 때 유용할 수 있어요.

숫자용 모듈을 정의하는 것부터 시작할게요.

trait Nums:
  // the type of numbers is left abstract
  type Num

  // some operations on numbers
  def lit(d: Double): Num
  def add(l: Num, r: Num): Num
  def mul(l: Num, r: Num): Num

Nums의 구체적인 구현은 생략할게요. 연습 삼아 type Num = Double로 두고 메서드들을 그에 맞게 구현해서 Nums를 직접 구현해 볼 수도 있어요.

숫자 추상화를 사용하는 프로그램은 이제 이런 타입을 가져요.

type Prog = (n: Nums) => n.Num => n.Num

val ex: Prog = nums => x => nums.add(nums.lit(0.8), x)

ex 같은 프로그램의 도함수(derivative)를 계산하는 함수의 타입은 이래요.

def derivative(input: Prog): Double

의존 함수 타입 덕분에, 이 함수를 서로 다른 프로그램들로 호출하는 게 아주 편리해요.

derivative { nums => x => x }
derivative { nums => x => nums.add(nums.lit(0.8), x) }
// ...

참고로, 위 인코딩에서 같은 프로그램은 이렇게 될 거예요.

derivative(new Prog {
  def apply(nums: Nums)(x: nums.Num): nums.Num = x
})
derivative(new Prog {
  def apply(nums: Nums)(x: nums.Num): nums.Num = nums.add(nums.lit(0.8), x)
})
// ...

컨텍스트 함수와의 조합 (Combination with Context Functions)

확장 메서드(extension method), 컨텍스트 함수(context function), 의존 함수의 조합은 라이브러리 설계자에게 강력한 도구가 돼요. 예를 들어 위 라이브러리를 다음과 같이 다듬을 수 있어요.

trait NumsDSL extends Nums:
  extension (x: Num)
    def +(y: Num) = add(x, y)
    def *(y: Num) = mul(x, y)

def const(d: Double)(using n: Nums): n.Num = n.lit(d)

type Prog = (n: NumsDSL) ?=> n.Num => n.Num
//                       ^^^
//     prog is now a context function that implicitly
//     assumes a NumsDSL in the calling context

def derivative(input: Prog): Double = ...

// notice how we do not need to mention Nums in the examples below?
derivative { x => const(1.0) + x }
derivative { x => x * x + const(2.0) }
// ...

아래 예시들에서 Nums를 언급할 필요가 없다는 게 보이죠? 호출 컨텍스트에 NumsDSL이 암시적으로 있다고 가정하는 컨텍스트 함수 덕분이에요.

더 알아보기