타입 클래스 지원 강화

타입 클래스 지원 강화 (Better Support for Type Classes)

Martin Odersky, 2024년 1월 8일, 2024년 4월 5일과 2024년 9월 30일에 편집됨

스칼라에서 타입 클래스(type class)는 특정 패턴을 말해요. 즉 하나의 타입 파라미터를 가진 트레잇(그 자체가 타입 클래스)을 정의하고, 그 트레잇의 특정 인스턴스화에 대한 given 인스턴스를 만들고, 그 트레잇을 추상화하는 using 절이나 컨텍스트 바운드(context bound)를 쓰는 패턴이에요. 이 글은 스칼라의 문법과 타입 규칙을 몇 가지 작고 합리적인 방식으로 다듬으면 타입 클래스 작업이 훨씬 편해지는 걸 보여줘요.

출처: Scala 3 Reference

본문

타입 클래스는 패턴으로서 전반적으로 잘 동작해요. 하지만 하스켈의 네이티브 구현, 스위프트의 프로토콜, 러스트의 트레잇과 비교해 보면 몇 가지 특성과 거친 모서리가 있어서, 표준 제네릭 프로그래밍 패턴에는 다소 번거롭고 제한적이게 만드는 면이 있어요. 스칼라 2의 implicits 이후로 많이 나아졌지만, 이 언어들에 따라잡으려면 아직 메워야 할 간격이 남아 있죠.

이 노트는 스칼라의 문법과 타입 규칙에 비교적 작고 합리적인 조정을 몇 가지 하면, 타입 클래스 작업이나 일반적인 제네릭 프로그래밍을 위한 훨씬 나은 구조를 얻을 수 있음을 보여줘요.

제안된 개선의 대부분은 구현되어 있고, 추가적인 실험용 언어 임포트 modularity가 있으면 소스 버전 future에서 사용할 수 있어요. 예를 들어 다음 명령을 쓰면 되죠.

  scala compile -source:future -language:experimental.modularity

여기서 설명하는 기능들은 스칼라 개선 프로세스(SIP)의 제안으로 만들려고 해요. 첫 번째 안건인 SIP 64는 몇 가지 문법 변경, 컨텍스트 바운드의 이름, 여러 컨텍스트 바운드, 지연된(deferred) givens를 다루며, 스칼라 3.6에 포함되도록 승인되었어요. 타입 클래스와 관련된 나머지 요소는 아래에서 설명해요. 타입 클래스와 직접 관련이 없는 제안 추가는 별도의 modularity 개선 페이지에서 다뤄요.

컨텍스트 바운드 일반화 (Generalizing Context Bounds)

스칼라 문법에서 타입 클래스 패턴이 등장하는 유일한 곳은 컨텍스트 바운드예요. 다음과 같은 컨텍스트 바운드

   def min[A: Ordering](x: List[A]): A

Ordering이 단일 타입 파라미터를 가진 트레잇이나 클래스여야 하며(그래야 타입 클래스가 됨), 그 파라미터를 인스턴스화하는 using 절로 확장된다는 요구사항이에요. min의 확장 형태는 다음과 같아요.

   def min[A](x: List[A])(using Ordering[A]): A

제안 타입 클래스가 타입 파라미터 대신 Self라는 추상 타입 멤버를 정의하는 것을 허용해요.

예시

  trait Ord:
    type Self

  trait SemiGroup:
    type Self
    extension (x: Self) def combine(y: Self): Self

  trait Monoid extends SemiGroup:
    def unit: Self
  object Monoid:
    def unit[M](using m: Monoid { type Self = M}): M

  trait Functor:
    type Self[A]
    extension [A](x: Self[A]) def map[B](f: A => B): Self[B]

  trait Monad extends Functor:
    def pure[A](x: A): Self[A]
    extension [A](x: Self[A])
      def flatMap[B](f: A => Self[B]): Self[B]
      def map[B](f: A => B) = x.flatMap(f `andThen` pure)

  def reduce[A: Monoid](xs: List[A]): A =
    xs.foldLeft(Monoid.unit)(_ `combine` _)

  trait ParserCombinator:
    type Self
    type Input
    type Result
    extension (self: Self)
      def parse(input: Input): Option[Result] = ...

  def combine[A: ParserCombinator, B: ParserCombinator { type Input = A.Input }] = ...

장점

  • 반복적인 타입 파라미터를 피하고, 본질적인 것, 즉 타입 클래스 계층에 집중하게 해줘요.
  • 타입 클래스로 쓰려는 트레잇을 분명히 드러내 줘요. Self 타입을 멤버로 가진 트레잇이 곧 타입 클래스예요.
  • 교차 타입(intersection types)으로 givens를 결합하는 집합형(aggregate) 타입 클래스를 만들 수 있게 해줘요.
  • 컨텍스트 바운드에서 정제(refinement)를 쓸 수 있게 해줘요. (위 combine 예시는 옛 방식, 즉 컨텍스트 바운드가 타입 생성자로 확장되던 방식으로 표현하기 매우 어색했을 거예요.)
  • Self 기반 컨텍스트 바운드는 파라미터 기반보다 의존적 타입 언어(dependently typed language)인 스칼라에 더 잘 맞아요. 주요 이유는 우리가 타입 생성자가 아니라 적절한 타입(proper type)을 다루기 때문이에요. 적절한 타입은 파라미터화, 교차, 정제가 가능하죠. 그래서 Self 기반 설계는 파라미터 기반보다 본질적으로 더 조합적(compositional)이에요.

세부 사항

트레잇이 타입 파라미터와 추상 Self 타입을 둘 다 가질 때, 컨텍스트 바운드는 Self 타입으로 해석돼요. 이렇게 하면 타입 파라미터를 가진 타입 클래스도 표현할 수 있어요.

trait Sequential[E]:
  type Self

이 경우,

[S: Sequential[Int]]

는 이렇게 해석되어야 해요.

[S](using Sequential[Int] { type Self = S })

이렇게가 아니라요.

[S](using Sequential[S])

논의

셀프 타입에 왜 This를 쓰지 않을까요? This라는 이름은 this의 타입이라는 뜻을 암시하지만, 타입 클래스 트레잇에서는 그렇지 않아요. Selfgiven 정의에서 트레잇의 구분된 멤버 타입을 구현하는 타입의 이름이에요. Self는 러스트와 스위프트 양쪽에서 여기서 쓰는 의미로 이미 확립된 용어예요.

Self 기반 설계에 대한 한 가지 반대 의견은 "다중 파라미터" 타입 클래스를 다루지 못한다는 거예요. 하지만 컨텍스트 바운드도 마찬가지예요! 스칼라에서 "다중 파라미터" 타입 클래스는 그냥 표준 메커니즘으로 합성할 수 있는 givens일 뿐이에요. 엄밀한 의미의 타입 클래스는 단 하나의 타입, 즉 트레잇의 구현 타입만 추상화해요.

보조 타입 별칭 is (Auxiliary Type Alias is)

표준 타입 별칭 isscala 패키지나 Predef에 도입해요. 다음과 같이 정의돼요.

  infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A }

이렇게 하면 인스턴스 정의와 using 절을 쓰기가 꽤 쾌적해져요. 예를 들어:

  given Int is Ord ...
  given Int is Monoid ...

  type Reader = [X] =>> Env => X
  given Reader is Monad ...

  object Monoid:
    def unit[M](using m: M is Monoid): M

(예시는 아래에서 더 나와요.)

컨텍스트 바운드의 더 나은 기본 이름 (Better Default Names for Context Bounds)

지금까지 타입 파라미터의 이름 없는 컨텍스트 바운드는 새로 합성된 fresh 이름을 받았어요. 하지만 제약된 타입 파라미터의 이름을 term 이름으로 변환한 것으로 받는 게 훨씬 유용해요. 이 말은 우리의 모노이드 reduce 메서드는 as 바인딩조차 필요 없다는 뜻이에요. 그냥 이렇게 표현하면 되거든요.

 def reduce[A : Monoid](xs: List[A]) =
    xs.foldLeft(A.unit)(_ `combine` _)

스칼라에서 우리는 이미 한 이름을 두 가지로 쓰는 데 익숙해요. 하나는 타입을, 다른 하나는 그와 연관된 값을 이름 짓는 경우죠. 예를 들어 클래스와 컴패니언 객체에 그런 관례를 써요. 돌이켜 보면 이 아이디어를 타입 파라미터까지 확장하는 건 당연해 보여요. 이전에 제기되지 않았다는 게 놀라울 정도예요.

제안 규칙

  • 컨텍스트 바운드 A : C as a에 대한 생성된 증거(evidence) 파라미터의 이름은 a예요.
  • as 바인딩이 없는 컨텍스트 바운드 A : C에 대한 생성된 증거의 이름은 A예요(term 이름으로 봄). 즉, A : CA : C as A와 동등해요.
  • 타입 파라미터에 컨텍스트 바운드가 여러 개 있다면, 예를 들어 A : {C_1, ..., C_n}인 경우, 각 컨텍스트 바운드 C_i에 대한 생성된 증거 파라미터는 fresh 합성 이름을 받아요. 단, 컨텍스트 바운드가 as 절을 가진다면 규칙 (1)이 적용돼요.

TODO: 컨텍스트 바운드 프록시 개념을 제시할 것.

기본 명명 관례는 이름 있는 컨텍스트 바운드의 필요성을 줄여줘요. 하지만 이름 있는 컨텍스트 바운드는 최소한 두 가지 이유로 여전히 필수적이에요.

  • 여러 컨텍스트 바운드에 이름을 부여하는 데 필요해요.
  • 이름 없는 단일 컨텍스트 바운드가 무엇으로 확장되는지 설명을 제공해요.

Singleton 고치기 (Fixing Singleton)

현재 Singleton을 타입 바운드로 다루는 방식이 깨져 있음을 우리는 알고 있어요. x.type | y.type <: Singleton이 합집합 타입의 서브타입 규칙에 의해 성립하지만, x.type | y.type이 분명히 싱글턴이 아니기 때문이에요.

더 나은 접근은 Singleton을 컴파일러가 특별히 해석하는 타입 클래스로 취급하는 거예요.

이건 Singleton을 다음과 같이 정의해서 하위 호환 방식으로 할 수 있어요.

trait Singleton:
  type Self

그러면 타당하지 않은 상한(upper bound) 대신 컨텍스트 바운드를 쓸 수 있어요.

def f[X: Singleton](x: X) = ...

컨텍스트 바운드는 컴파일러가 특별히 취급해서, 런타임에 using 절을 생성하지 않아요(지워진 정의(erased definitions) 메커니즘을 쓰면 간단해요).

정밀 타입 (Precise Typing)

이 접근은 정밀한(precise) 타입 변수를 표현하는 문제에 대한 해법도 제시해요. Precise라는 또 다른 특별 타입 클래스를 도입해서 이렇게 쓰면 돼요.

def f[X: Precise](x: X) = ...

Singleton 바운드처럼 Precise 바운드도 타입 변수 X의 추론된 인스턴스에서 싱글턴 타입이나 합집합 타입의 자동 확대(widening)를 비활성화해요. 다만 타입 인자가 반드시 싱글턴일 필요는 없다는 점이 달라요.

예시 (Examples)

예시 1

다음은 이 노트 시작 부분에서 대부분 이미 소개한 표준 타입 클래스들에, 관련 인스턴스 givens와 일부 테스트 코드를 붙인 거예요.

  // Type classes

  trait Ord:
    type Self
    extension (x: Self)
      def compareTo(y: Self): Int
      def < (y: Self): Boolean = compareTo(y) < 0
      def > (y: Self): Boolean = compareTo(y) > 0
      def <= (y: Self): Boolean = compareTo(y) <= 0
      def >= (y: Self): Boolean = compareTo(y) >= 0
      def max(y: Self): Self = if x < y then y else x

  trait Show:
    type Self
    extension (x: Self) def show: String

  trait SemiGroup:
    type Self
    extension (x: Self) def combine(y: Self): Self

  trait Monoid extends SemiGroup:
    def unit: Self

  trait Functor:
    type Self[A] // Here, Self is a type constructor with parameter A
    extension [A](x: Self[A]) def map[B](f: A => B): Self[B]

  trait Monad extends Functor:
    def pure[A](x: A): Self[A]
    extension [A](x: Self[A])
      def flatMap[B](f: A => Self[B]): Self[B]
      def map[B](f: A => B) = x.flatMap(f `andThen` pure)

  // Instances

  given Int is Ord:
    extension (x: Int)
      def compareTo(y: Int) =
        if x < y then -1
        else if x > y then +1
        else 0

  given [T: Ord] => List[T] is Ord:
    extension (xs: List[T]) def compareTo(ys: List[T]): Int =
      (xs, ys) match
      case (Nil, Nil) => 0
      case (Nil, _) => -1
      case (_, Nil) => +1
      case (x :: xs1, y :: ys1) =>
        val fst = x.compareTo(y)
        if (fst != 0) fst else xs1.compareTo(ys1)

  given List is Monad:
    extension [A](xs: List[A])
      def flatMap[B](f: A => List[B]): List[B] =
        xs.flatMap(f)
    def pure[A](x: A): List[A] =
      List(x)

  type Reader[Ctx] = [X] =>> Ctx => X

  given [Ctx] => Reader[Ctx] is Monad:
    extension [A](r: Ctx => A)
      def flatMap[B](f: A => Ctx => B): Ctx => B =
        ctx => f(r(ctx))(ctx)
    def pure[A](x: A): Ctx => A =
      ctx => x

  // Usages

  extension (xs: Seq[String])
    def longestStrings: Seq[String] =
      val maxLength = xs.map(_.length).max
      xs.filter(_.length == maxLength)

  extension [M[_]: Monad, A](xss: M[M[A]])
    def flatten: M[A] =
      xss.flatMap(identity)

  def maximum[T: Ord](xs: List[T]): T =
    xs.reduce(_ `max` _)

  given descending: [T: Ord] => T is Ord:
    extension (x: T) def compareTo(y: T) = T.compareTo(y)(x)

  def minimum[T: Ord](xs: List[T]) =
    maximum(xs)(using descending)

Reader 타입은 좀 얽혀 있어요. 이건 타입 클래스(파라미터화된 문법으로 작성된)인데, 컨텍스트 Ctx를 고정하고 Reader가 컨텍스트 Ctx를 받아 X를 반환하는 X에 대한 다형 함수 타입이 되게 한 거예요. 이런 타입 클래스는 모나딕 이펙트 시스템에서 흔히 쓰여요.

예시 2

다음은 @LPTK(이슈 #10929)가 기여한 코드인데, 처음에는 참조가 제대로 추적되지 않아서 동작하지 않았어요. 아래 버전은 명시적인 tracked 파라미터를 추가해서 컴파일되게 만든 거예요.

infix abstract class TupleOf[T, +A]:
  type Mapped[+A] <: Tuple
  def map[B](x: T)(f: A => B): Mapped[B]

object TupleOf:

  given TupleOf[EmptyTuple, Nothing] with
    type Mapped[+A] = EmptyTuple
    def map[B](x: EmptyTuple)(f: Nothing => B): Mapped[B] = x

  given [A, Rest <: Tuple](using tracked val tup: Rest TupleOf A): TupleOf[A *: Rest, A] with
    type Mapped[+A] = A *: tup.Mapped[A]
    def map[B](x: A *: Rest)(f: A => B): Mapped[B] =
      f(x.head) *: tup.map(x.tail)(f)

꽤 얽힌 문법 때문에 코드를 이해하기 어렵다는 걸 주목해 주세요. 다음은 같은 예시를 새 타입 클래스 문법으로 쓴 건데, 역시 올바르게 컴파일돼요.

//> using options -language:experimental.modularity -source future

trait TupleOf[+A]:
  type Self
  type Mapped[+A] <: Tuple
  def map[B](x: Self)(f: A => B): Mapped[B]

object TupleOf:

  given EmptyTuple is TupleOf[Nothing]:
    type Mapped[+A] = EmptyTuple
    def map[B](x: EmptyTuple)(f: Nothing => B): Mapped[B] = x

  given [A, Rest <: Tuple : TupleOf[A]] => A *: Rest is TupleOf[A]:
    type Mapped[+A] = A *: Rest.Mapped[A]
    def map[B](x: A *: Rest)(f: A => B): Mapped[B] =
      f(x.head) *: Rest.map(x.tail)(f)

특히 다음 점들을 주목해 보세요.

  • 원래 코드에서는 TupleOf가 두 개의 타입 파라미터를 포함하고 있어서 타입 클래스인지 분명하지 않았어요. 그중 하나는 인스턴스 타입 Self의 역할을 했는데요, 새 버전은 훨씬 분명해요. TupleOfSelf에 대한 타입 클래스이면서, 추가 파라미터로 모든 튜플 요소의 공통 타입을 가지는 거죠.
  • 두 given 정의는 옛 코드에서 난해해 보였어요. 새 코드의 버전은 어떤 종류의 인스턴스를 정의하는지 분명히 보여줘요.
    • EmptyTupleNothing의 튜플이에요.
    • RestA의 튜플이라면, A *: RestA의 튜플이에요.
  • using 절에서 파라미터 인스턴스의 이름을 붙일 필요가 없어요. 컨텍스트 바운드 증거의 기본 명명 방식이 잘 동작하고, 더 간결해요.
  • 암시적 파라미터를 tracked로 수동 선언할 필요가 없어요. 컨텍스트 바운드가 자동으로 제공해 주거든요.
  • 새 코드의 모든 것이 관용적인(idiomatic) 스칼라 3처럼 느껴져요. 반면 원래 코드는 given 정의 앞에 with를 요구하는 어색한 모서리 케이스를 보여주고 있죠.

예시 3

Dimi Racordon은 입력과 결과에 의존적 타입 멤버를 쓰는 파서 콤비네이터를 스칼라에서 정의하려 했어요. 타입 클래스 제약의 기본 예시로 의도했지만, 현재 스칼라에서는 동작하지 않았어요.

다음은 이 문제를 새 문법으로 푼 거예요. Dimi의 원래 버전(결국 동작하지 못한)과 비교해 이 문법이 얼마나 훨씬 분명한지 보세요.

/** A parser combinator */
trait Combinator:
  type Self

  type Input
  type Result

  extension (self: Self)
    /** Parses and returns an element from input `in` */
    def parse(in: Input): Option[Result]
end Combinator

case class Apply[I, R](action: I => Option[R])
case class Combine[A, B](a: A, b: B)

given [I, R] => Apply[I, R] is Combinator:
  type Input = I
  type Result = R
  extension (self: Apply[I, R])
    def parse(in: I): Option[R] = self.action(in)

given [A: Combinator, B: Combinator { type Input = A.Input }]
    => Combine[A, B] is Combinator:
  type Input = A.Input
  type Result = (A.Result, B.Result)
  extension (self: Combine[A, B])
    def parse(in: Input): Option[Result] =
      for
        x <- self.a.parse(in)
        y <- self.b.parse(in)
      yield (x, y)

이 예시는 이제 마땅히 그래야 할 만큼 직관적으로 표현돼요.

  • CombinatorInputResult라는 두 연관 타입과 parse 메서드를 가진 타입 클래스예요.
  • ApplyCombine은 파서 콤비네이터를 나타내는 두 데이터 생성자예요. 뒤따르는 두 given 선언에서 Combinator로 선언돼요.
  • Apply의 parse 메서드는 action 함수를 입력에 적용해요.
  • Combine[A, B]AB가 같은 종류의 Input을 처리하는 파서 콤비네이터일 때 성립하는 파서 콤비네이터예요. Combine[A, B]의 입력 타입도 그 Input이죠. Result 타입은 ABResult 타입의 쌍이에요. 결과는 간단한 for-표현식으로 생성돼요.

심각한 비틀기가 필요했던 원래 예시와 비교하면, 이제 완전히 직관적이에요.

Note 1: 개선 여지도 탐구할 수 있어요. 예를 들어 이것을 완전히 순수 함수형으로 만드는 것 같은 거죠. 하지만 그건 여기 데모의 목적이 아니에요. 여기서는 원래 예시를 가져와서 새 구성으로 어떻게 동작하게 만들고 더 분명하게 표현할 수 있는지 보여주고 싶었어요.

Note 2: 스위프트 스타일의 동등성 제약(equality constraint)을 추가하면 표기법을 더 개선할 수 있어요. 이는 SML의 *공유 제약(sharing constraints)*과 닮았죠. 두 번째 given에 적용한 가상의 문법은 다음과 같을 거예요.

given [A: Combinator, B: Combinator with A.Input == B.Input]
    => Combine[A, B] is Combinator:

이 변형은 동등성 제약을 대칭적으로 만들어서 미적으로 매력적이에요. 원래 버전은 두 번째 타입 파라미터 바운드에 비대칭 정제를 써야 했거든요. 지금으로서는 그런 제약은 구현되지도, 제안되지도 않았어요. 미래 작업의 가능성으로 남겨 두는 거예요. @mbovel과 @Sporarum의 정제 타입 작업과의 유사성도 주목할 만한데, 거기서는 비슷한 with 절이 term 파라미터에 나타날 수 있죠. 그 작업이 진행되면 타입 파라미터에도 with 절을 도입하는 문제를 다시 검토할 수 있어요.

예시 4

Dimi Racordon은 타입 클래스 기반 Hylo 표준 라이브러리의 핵심 요소를 스칼라로 포팅하려 했어요. 어느 정도 동작했지만, 표현할 수 없는 것도 있었고 더 많은 것을 어색하게만 표현할 수 있었어요.

여기서 제안한 개선으로 그 라이브러리는 꽤 분명하고 직관적으로 표현할 수 있어요. 자세한 내용은 이 PR의 tests/pos/hylolib를 보세요.

타입 클래스와 무관한 제안 개선 (Suggested Improvement unrelated to Type Classes)

다음 개선은 타입 클래스에 대한 제안된 변경과 함께라면 의미가 있어요. 하지만 이 제안의 일부는 아니고 아직 구현되지도 않았어요.

패턴에서도 as 사용하기 (Using as also in Patterns)

이제 as를 후위 바인더로 쓰는 선례가 꽤 있으니, @ 대신 패턴에서도 쓰자는 제안을 다시 꺼내고 싶어요. @는 폐기(deprecate)되어야 하죠.

예시:

  xs match
    case (Person(name, age) as p) :: rest => ...

  tp match
    case Param(tl, _) :: _ as tparams => ...

  val x :: xs1 as xs = ys.checkedCast

이것들은 @를 쓰는 이전 문법을 대체해요.

  xs match
    case p @ Person(name, age) :: rest => ...

  tp match
    case tparams @ (Param(tl, _) :: _) => ...

  val xs @ (x :: xs1) = ys.checkedCast

장점: @ 같은 발음할 수 없고 비표준인 기호가 없다. 더 규칙적이다.

일반적으로 우리는 단독으로도 쓰일 수 있는 어떤 엔티티에 이름을 붙일 때 as name을 쓰고 싶어요.

제안된 문법 변경

Pattern2          ::=  InfixPattern ['as' id]

요약 (Summary)

스칼라 3에 몇 가지 조정을 제안했는데, 이는 모듈형의 타입 클래스 기반 제네릭 프로그래밍에 대한 유용성을 높여줄 거예요. 제안된 변경은 다음과 같아요.

  • Self 멤버 타입을 정의하는 클래스에 대한 컨텍스트 바운드를 허용해요.
  • 미리 정의된 타입 별칭 is를 추가해요.
  • 타입 파라미터나 멤버 T가 컨텍스트 바운드 CB를 가질 때, CB의 증인(witness)의 기본 이름으로 T를 사용해요.
  • Singleton을 정리하고, 타입 변수의 확대되지 않는 인스턴스화를 위한 새 트레잇 Precise를 추가해요.

결론 (Conclusion)

제네릭 프로그래밍은 여러 언어로 표현할 수 있어요. 예를 들어 하스켈의 타입 클래스, 러스트의 트레잇, 스위프트의 프로토콜, C++의 개념(concepts) 같은 거죠. 이들 각각은 표현식과 타입과는 다른, 꽤 무거운 새 구성들의 집합으로 만들어져요. 이와 대조적으로 스칼라의 동등한 해법은 일반 타입에 의존해요. 타입 클래스는 그저 Self 타입 멤버를 정의하는 트레잇일 뿐이에요.

제안된 구조는 스위프트의 프로토콜이나 러스트의 트레잇과 비슷한 표현력을 가져요. 둘 다 Jeremy Siek의 박사 논문 "A language for generic programming"의 영향을 크게 받았는데, 이 논문은 원래 C++의 개념을 구현하는 방법으로 제안됐죠. C++는 Siek의 접근을 따르지 않았지만, 스위프트와 러스트는 따랐어요.

Siek의 논문과 러스트·스위프트의 형식적 처리에서 타입 클래스 개념은 term과 타입의 표현을 가진 명시적 사전(dictionary)의 저수준 언어로 매핑해서 설명돼요. 결정적으로 그 저수준 언어는 타입 표현이 term 사전에 매핑되므로 소스 언어 자체에서 세밀함(granularity)의 손실 없이 표현할 수 없어요. 이와 대조적으로 현재 제안은 타입 클래스 개념을 다른 잘 타입화된 스칼라 구성으로 확장하며, 이것은 궁극적으로 잘 타입화된 DOT 프로그램에 매핑돼요. 타입 클래스는 스칼라에서 이미 표현할 수 있는 것에 대한 편리한 표기법일 뿐이에요. 그런 의미에서 우리는 *확장 가능한 언어(scalable language)*라는 철학, 즉 작은 핵심이 광범위한 고급 사용 사례를 지원할 수 있다는 철학에 충실해요.