TypeTest
TypeTest
런타임에서 타입 검사(type test)를 해야 하는 상황은 두 가지가 있는데요, 추상 타입에 대한 검사는 erasure 때문에 그냥은 수행할 수 없어요. 이때 TypeTest가 그 검사를 가능하게 해 줍니다.
본문
패턴 매칭을 할 때 런타임 타입 검사를 반드시 수행해야 하는 상황이 두 가지 있어요. 첫 번째는 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: Y는 tt(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가 됐을 거라는 점을 참고하세요.