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.