TypeTest
TypeTest
When pattern matching there are two situations where a runtime type test must be performed. The first case is an explicit type test using the ascription pattern notation.
(x: X) match
case y: Y =>
The second case is when an extractor takes an argument that is not a subtype of the scrutinee type.
(x: X) match
case y @ Y(n) =>
object Y:
def unapply(x: Y): Some[Int] = ...
In both cases, a class test will be performed at runtime. But when the type test is on an abstract type (type parameter or type member), the test cannot be performed because the type is erased at runtime.
A TypeTest
can be provided to make this test possible.
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 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
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.