在 GitHub 上编辑此页面

匹配类型

匹配类型会根据其被检查值的类型缩减为其右侧之一。例如

type Elem[X] = X match
  case String => Char
  case Array[t] => t
  case Iterable[t] => t

这定义了一个按如下方式缩减的类型

Elem[String]       =:=  Char
Elem[Array[Int]]   =:=  Int
Elem[List[Float]]  =:=  Float
Elem[Nil.type]     =:=  Nothing

此处 =:= 表示左右两侧是彼此的互为子类型。

一般来说,匹配类型的形式为

S match { P1 => T1 ... Pn => Tn }

其中ST1、...、Tn是类型,P1、...、Pn是类型模式。模式中的类型变量以小写字母开头,这很常见。

匹配类型可以构成递归类型定义的一部分。示例

type LeafElem[X] = X match
  case String => Char
  case Array[t] => LeafElem[t]
  case Iterable[t] => LeafElem[t]
  case AnyVal => X

递归匹配类型定义也可以给出上限,如下所示

type Concat[Xs <: Tuple, +Ys <: Tuple] <: Tuple = Xs match
  case EmptyTuple => Ys
  case x *: xs => x *: Concat[xs, Ys]

在此定义中,Concat[A, B]的每个实例(无论是否可约)都已知是Tuple的子类型。这是使递归调用x *: Concat[xs, Ys]类型检查所必需的,因为*:要求Tuple作为其右操作数。

依赖类型

匹配类型可用于定义依赖类型的方法。例如,以下是上面定义的LeafElem类型的值级对应项(请注意将匹配类型用作返回类型)

def leafElem[X](x: X): LeafElem[X] = x match
  case x: String      => x.charAt(0)
  case x: Array[t]    => leafElem(x(0))
  case x: Iterable[t] => leafElem(x.head)
  case x: AnyVal      => x

仅在满足以下条件时才使用匹配表达式的这种特殊类型模式

  1. 匹配表达式模式没有保护
  2. 匹配表达式被检查的类型是匹配类型被检查的类型的子类型
  3. 匹配表达式和匹配类型具有相同数量的用例
  4. 匹配表达式模式全部是类型模式,并且这些类型=:=与匹配类型中对应的类型模式

因此,虽然用例主体应具有与相应的匹配类型用例右侧的类型,但这并不意味着匹配类型参数受到约束。使用该示例,最后一个用例主体必须符合 X,但这并不将 X 限制为 AnyVal,因此主体内的 LeafElem[X] 不会减少;它将保持不变,因此只是一个抽象类型。

匹配类型的表示

匹配类型的内部表示

S match { P1 => T1 ... Pn => Tn }

Match(S, C1, ..., Cn) <: B,其中每个用例Ci的形式为

[Xs] =>> P => T

在此,[Xs] 是模式 Pi 中绑定变量的类型参数子句。如果一个 case 中没有绑定类型变量,则省略类型参数子句,只保留函数类型 P => T。因此,每个 case 要么是单一函数类型,要么是单一函数类型的类型 lambda。

B 是匹配类型的声明上限,如果没有给出此类上限,则为 Any。在讨论中无关紧要的地方,我们将省略它。被检视类型、绑定类型和模式类型都必须是一阶类型。

匹配类型规约

匹配类型规约遵循匹配表达式的语义,即,形式为 S match { P1 => T1 ... Pn => Tn } 的匹配类型当且仅当 s: S match { _: P1 => T1 ... _: Pn => Tn } 对所有 s: S 求值为类型 Ti 的值时,规约为 Ti

编译器实现以下规约算法

  • 如果被检视类型 S 是一个空值集(例如 NothingString & Int),则不规约。
  • 按顺序考虑每个模式 Pi
    • 如果 S <: Pi 规约为 Ti
    • 否则,尝试构造一个证明,证明 SPi 是不相交的,或者换句话说,证明类型 S 的任何值 s 也不是类型 Pi
    • 如果找到此类证明,则继续下一个 case(Pi+1),否则,不规约。

不相交性证明依赖于 Scala 类型的以下属性

  1. 类的单一继承
  2. 最终类不能扩展
  3. 具有不同值的常量类型不相交
  4. 到不同值的单例路径不相交,例如 object 定义或单例枚举 case。

计算 S <: Pi 时,模式中的类型参数被最小化实例化。如果 Xs 中所有协变和非变异地出现在 Is 中的类型变量尽可能小,并且 Xs 中所有逆变地出现在 Is 中的类型变量尽可能大,则实例化 IsXs 来说是最小的。在此,“小”和“大”相对于 <: 理解。但是,如果包含它的模式与协变或逆变位置的 lambda case 匹配,则类型参数不会“大”。

为简单起见,到目前为止,我们省略了约束处理。子类型测试的完整表述将它们描述为从约束和一对类型到成功和新约束或失败的函数。在规约的上下文中,子类型测试 S <: [Xs := Is] P 被理解为不改变输入约束中所有变量的界限,即,不能通过将被检视类型与模式匹配来实例化约束中的现有变量。

匹配类型的子类型规则

以下规则适用于匹配类型。为了简单起见,我们省略了环境和约束。

  1. 第一条规则是两个匹配类型之间的结构比较

    S match { P1 => T1 ... Pm => Tm }  <:  T match { Q1 => U1 ... Qn => Un }
    

    如果

    S =:= T,  m >= n,  Pi =:= Qi and Ti <: Ui for i in 1..n
    

    即被检查者和模式必须相等,并且相应的正文必须是子类型。不允许重新排序情况,但子类型可以比超类型有更多的情况。

  2. 第二条规则指出,匹配类型及其 redux 是相互子类型。

    S match { P1 => T1 ... Pn => Tn }  <:  U
    U  <:  S match { P1 => T1 ... Pn => Tn }
    

    如果

    S match { P1 => T1 ... Pn => Tn } 归约为 U

  3. 第三条规则指出,匹配类型符合其上限

    (S match { P1 => T1 ... Pn => Tn } <: B)  <:  B
    

终止

匹配类型定义可以是递归的,这意味着在归约匹配类型时可能会陷入无限循环。

由于归约与子类型化相关,因此我们已经有了循环检测机制。因此,以下内容将已经给出一个合理的错误消息

type L[X] = X match
  case Int => L[X]

def g[X]: L[X] = ???
|  val x: Int = g[Int]
   |                ^
   |Recursion limit exceeded.
   |Maybe there is an illegal cyclic reference?
   |If that's not the case, you could also try to
   |increase the stacksize using the -Xss JVM option.
   |A recurring operation is (inner to outer):
   |
   |  subtype LazyRef(Test.L[Int]) <:< Int

在内部,Scala 编译器通过将选定的堆栈溢出转换为类型错误来检测这些循环。如果在子类型化期间发生堆栈溢出,则将捕获异常并将其转换为编译时错误,该错误指示导致溢出的子类型测试的跟踪,而不会显示完整的堆栈跟踪。

匹配类型方差

匹配类型中的所有类型位置(被检查者、模式、正文)都被认为是不变的。