TypeTest

TypeTest

런타임에서 타입 검사(type test)를 해야 하는 상황은 두 가지가 있는데요, 추상 타입에 대한 검사는 erasure 때문에 그냥은 수행할 수 없어요. 이때 TypeTest가 그 검사를 가능하게 해 줍니다.

출처: Scala 3 Reference

본문

패턴 매칭을 할 때 런타임 타입 검사를 반드시 수행해야 하는 상황이 두 가지 있어요. 첫 번째는 ascription 패턴 표기법을 쓰는 명시적 타입 검사예요.

(x: X) match
  case y: Y =>

두 번째는 추출기(extractor)가 scrutinee 타입의 서브타입이 아닌 인자를 받는 경우예요.

(x: X) match
  case y @ Y(n) =>

object Y:
  def unapply(x: Y): Some[Int] = ...

두 경우 모두 런타임에 클래스 검사가 수행돼요. 그런데 타입 검사 대상이 추상 타입(타입 파라미터나 타입 멤버)이면 그 타입이 런타임에 erasure되기 때문에 검사를 수행할 수 없어요. 이 검사를 가능하게 해 주는 게 TypeTest예요.

package scala.reflect

trait TypeTest[-S, T]:
  def unapply(s: S): Option[s.type & T]

이것은 인자가 T라면 그 인자를 T로 타이핑해 돌려주는 추출기를 제공해요. 이를 이용해 타입 검사를 인코딩할 수 있어요.

def f[X, Y](x: X)(using tt: TypeTest[X, Y]): Option[Y] = x match
  case tt(x @ Y(1)) => Some(x)
  case tt(x) => Some(x)
  case _ => None

문법적 부담을 피하기 위해, 컴파일러는 타입 검사 대상이 추상 타입임을 감지하면 타입 검사를 자동으로 찾아요. 즉 범위 안에 컨텍스트 TypeTest[X, Y]가 있다면 x: Ytt(x)로, x @ Y(_)tt(x @ Y(_))로 변환돼요. 아까 코드는 다음 코드와 동등해요.

def f[X, Y](x: X)(using TypeTest[X, Y]): Option[Y] = x match
  case x @ Y(1) => Some(x)
  case x: Y => Some(x)
  case _ => None

타입 검사를 런타임 클래스 검사로 바로 수행할 수 있는 호출 지점에서는 다음과 같이 타입 검사를 만들 수 있어요.

val tt: TypeTest[Any, String] =
  new TypeTest[Any, String]:
    def unapply(s: Any): Option[s.type & String] = s match
      case q: (s.type & String) => Some(q)
      case _ => None

f[AnyRef, String]("acb")(using tt)

범위 안에 아무것도 없으면 컴파일러는 다음과 같이 타입 검사 인스턴스를 합성해요.

new TypeTest[A, B]:
  def unapply(s: A): Option[s.type & B] = s match
    case s: B => Some(s)
    case _ => None

타입 검사를 수행할 수 없다면 case s: B => ... 테스트에 unchecked 경고가 발생해요. 가장 흔한 TypeTest 인스턴스는 임의 파라미터를 받는 것, 즉 TypeTest[Any, T] 형태예요. 그런 인스턴스를 컨텍스트 바운드에서 바로 사용할 수 있도록 다음 별칭을 제공해요.

package scala.reflect

type Typeable[T] = TypeTest[Any, T]

이 별칭은 이렇게 사용할 수 있어요.

def f[T: Typeable]: Boolean =
  "abc" match
    case x: T => true
    case _ => false

f[String] // true
f[Int] // false

TypeTest와 ClassTag

TypeTest는 이전에 ClassTag.unapply가 담당하던 기능을 대체해요. ClassTag 인스턴스를 쓰는 건 타입의 클래스 성분만 검사할 수 있기 때문에 unsound했어요. TypeTest는 그 unsoundness를 해결해 줘요. ClassTag 타입 검사는 여전히 지원되지만, 3.0 이후로는 경고가 발생할 거예요.

예시 (Example)

다음은 두 개의 given 인스턴스, 즉 TypeTest[Nat, Zero]TypeTest[Nat, Succ] 타입을 제공하는 페아노 수(Peano numbers)의 추상 정의예요.

import scala.reflect.*

trait Peano:
  type Nat
  type Zero <: Nat
  type Succ <: Nat

  def safeDiv(m: Nat, n: Succ): (Nat, Nat)

  val Zero: Zero

  val Succ: SuccExtractor
  trait SuccExtractor:
    def apply(nat: Nat): Succ
    def unapply(succ: Succ): Some[Nat]

  given typeTestOfZero: TypeTest[Nat, Zero]
  given typeTestOfSucc: TypeTest[Nat, Succ]

그리고 Int 타입을 기반으로 한 페아노 수 구현과 함께

object PeanoInt extends Peano:
  type Nat  = Int
  type Zero = Int
  type Succ = Int

  def safeDiv(m: Nat, n: Succ): (Nat, Nat) = (m / n, m % n)

  val Zero: Zero = 0

  val Succ: SuccExtractor = new:
    def apply(nat: Nat): Succ = nat + 1
    def unapply(succ: Succ) = Some(succ - 1)

  def typeTestOfZero: TypeTest[Nat, Zero] = new:
    def unapply(x: Nat): Option[x.type & Zero] =
      if x == 0 then Some(x) else None

  def typeTestOfSucc: TypeTest[Nat, Succ] = new:
    def unapply(x: Nat): Option[x.type & Succ] =
      if x > 0 then Some(x) else None

다음과 같은 프로그램을 작성할 수 있어요.

@main def test =
  import PeanoInt.*

  def divOpt(m: Nat, n: Nat): Option[(Nat, Nat)] =
    n match
      case Zero => None
      case s @ Succ(_) => Some(safeDiv(m, s))

  val two = Succ(Succ(Zero))
  val five = Succ(Succ(Succ(two)))

  println(divOpt(five, two))  // prints "Some((2,1))"
  println(divOpt(two, five))  // prints "Some((0,2))"
  println(divOpt(two, Zero))  // prints "None"

TypeTest[Nat, Succ]가 없다면 Succ.unapply(nat: Succ) 패턴은 unchecked가 됐을 거라는 점을 참고하세요.