Link Search Menu Expand Document

TypeTest

在模式匹配时,有两种情况必须执行运行时类型测试。 第一种情况是使用 ascription pattern 符号的显式类型测试。

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

第二种情况是提取器接受的参数不是 scrutinee 类型的子类型。

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

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

在这两种情况下,类型测试都会在运行时执行。但在抽象类型(类型参数或类型成员)上无法执行类型测试, 因为类型在运行时被擦除。

可以通过提供 TypeTest 使得该测试可以进行。

package scala.reflect

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

It provides an extractor that returns its argument typed as a T if the argument is a T. It can be used to encode a type test.

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
}

To avoid the syntactic overhead the compiler will look for a type test automatically if it detects that the type test is on abstract types. This means that x: Y is transformed to tt(x) and x @ Y(_) to tt(x @ Y(_)) if there is a contextual TypeTest[X, Y] in scope. The previous code is equivalent to

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
}

We could create a type test at call site where the type test can be performed with runtime class tests directly as follows

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

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

The compiler will synthesize a new instance of a type test if none is found in scope as:

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

If the type tests cannot be done there will be an unchecked warning that will be raised on the case s: B => test.

The most common TypeTest instances are the ones that take any parameters (i.e. TypeTest[Any, T]). To make it possible to use such instances directly in context bounds we provide the alias

package scala.reflect

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

This alias can be used as

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

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

TypeTest and ClassTag

TypeTest is a replacement for functionality provided previously by ClassTag.unapply. Using ClassTag instances was unsound since classtags can check only the class component of a type. TypeTest fixes that unsoundness. ClassTag type tests are still supported but a warning will be emitted after 3.0.

Example

Given the following abstract definition of Peano numbers that provides two given instances of types TypeTest[Nat, Zero] and TypeTest[Nat, Succ]

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]
}

together with an implementation of Peano numbers based on type 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) Some(x) else None
   }

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

it is possible to write the following program

@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"
}

Note that without the TypeTest[Nat, Succ] the pattern Succ.unapply(nat: Succ) would be unchecked.