Проверка типа

При сопоставлении с образцом есть две ситуации, когда необходимо выполнить проверку типа во время выполнения. Первый случай — это явная проверка типа с использованием нотации шаблона атрибуции.

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

Второй случай — когда экстрактор принимает аргумент, не являющийся подтипом контролируемого типа.

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

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

В обоих случаях проверка класса будет выполняться во время выполнения. Но когда проверка типа относится к абстрактному типу (параметру типа или члену типа), проверка не может быть выполнена, поскольку тип стирается во время выполнения.

Проверку типов в этих случаях можно выполнять если предоставлен trait 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

Чтобы избежать синтаксических издержек, компилятор будет автоматически искать проверку типа, если обнаружит, что проверка типа относится к абстрактным типам. Это означает, что x: Y трансформируется в tt(x) и x @ Y(_) в tt(x @ Y(_)), если в области видимости доступен контекст TypeTest[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 s: String => Some(s)
      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 => будет выдано непроверенное предупреждение.

Наиболее распространенными экземплярами TypeTest являются те, которые принимают любые параметры (т.е. TypeTest[Any, T]). Чтобы можно было использовать такие экземпляры непосредственно в границах контекста, предоставляется псевдоним

package scala.reflect

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

Этот псевдоним можно использовать так:

import scala.reflect.Typeable

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

f[String]
// res0: Boolean = true
f[Int]
// res1: Boolean = false

Пример

Учитывая следующее абстрактное определение чисел Пеано, которое предоставляет два given экземпляра типов TypeTest[Nat, Zero] и 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]

вместе с реализацией чисел Пеано на основе типа 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

можно написать следующую программу:

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))
// two: Int = 2
val five = Succ(Succ(Succ(two)))
// five: Int = 5

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

Обратите внимание, что без TypeTest[Nat, Succ] паттерн Succ.unapply(nat: Succ) был бы unchecked.


Ссылки: