在 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 中繫結變數的類型參數子句。如果案例中沒有繫結類型變數,則省略類型參數子句,只保留函數類型 P => T。因此,每個案例都是一元函數類型或一元函數類型上的類型 lambda。

B 是比對類型的宣告上限,如果沒有給定此類上限,則為 Any。在討論中不重要的位置,我們將省略它。受檢視者、繫結和樣式類型都必須是一階類型。

比對類型還原

比對類型還原遵循比對表達式的語意,也就是說,形式為 S match { P1 => T1 ... Pn => Tn } 的比對類型會還原為 Ti,當且僅當 s: S match { _: P1 => T1 ... _: Pn => Tn } 對所有 s: S 評估為類型 Ti 的值時。

編譯器實作下列簡約演算法

  • 如果檢視類型 S 是空值集合(例如 NothingString & Int),則不簡約。
  • 依序考慮每個模式 Pi
    • 如果 S <: Pi 簡約為 Ti
    • 否則,嘗試建構一個證明,證明 SPi 是不相交的,或者換句話說,類型 S 的值 s 也不屬於類型 Pi
    • 如果找到此證明,則繼續下一個案例 (Pi+1),否則不簡約。

不相交證明依賴於 Scala 類型的下列屬性

  1. 類別的單一繼承
  2. 最終類別無法延伸
  3. 具有不同值的常數類型不相交
  4. 通往不同值的單例路徑不相交,例如 object 定義或單例列舉案例。

計算 S <: Pi 時,模式中的類型參數會以最少實例化。如果 Xs 中所有在 Is 中以共變和非變異出現的類型變數都盡可能小,而 Xs 中所有在 Is 中以反變異出現的類型變數都盡可能大,則實例化 Is 對於 Xs 來說是最小的。在此,「小」和「大」是相對於 <: 來理解的。但是,如果包含它的模式與共變或反變異位置中的 lambda 案例相符,則類型參數不會「大」。

為簡化起見,到目前為止我們已省略約束處理。子類別測試的完整公式將它們描述為從約束和一對類型到成功和新約束或失敗的函數。在簡約的背景下,子類別測試 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. 第二個規則指出相符類型及其簡約是相互的子類別。

    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 編譯器會透過將選定的堆疊溢位轉換為類型錯誤來偵測這些循環。如果在子類型化期間發生堆疊溢位,系統會擷取例外狀況並將其轉換為編譯時期錯誤,指出導致溢位的子類型測試追蹤,而不會顯示完整的堆疊追蹤。

比對類型變異

比對類型中的所有類型位置(scrutinee、模式、主體)都視為不變。