第 5 章 多型變體

在第 1.4 節中介紹的變體是建立資料結構和演算法的強大工具。然而,當用於模組化程式設計時,它們有時會缺乏彈性。這是因為每個建構子在定義和使用時都被指定為唯一的型別。即使相同的名稱出現在多種類型的定義中,建構子本身也只屬於一個類型。因此,無法決定給定的建構子屬於多個類型,或者認為某個類型的數值屬於具有更多建構子的其他類型。

使用多型變體,此原始假設將被移除。也就是說,變體標籤不屬於任何特定的類型,類型系統只會根據其使用情況檢查它是否是可接受的值。您無需在使用變體標籤之前定義類型。變體類型將針對其每次使用獨立推斷。

1 基本用法

在程式中,多型變體的使用方式與一般的變體相同。您只需在其名稱前加上反引號字元 ` 即可。

# [`On; `Off];;
- : [> `Off | `On ] list = [`On; `Off]
# `Number 1;;
- : [> `Number of int ] = `Number 1
# let f = function `On -> 1 | `Off -> 0 | `Number n -> n;;
val f : [< `Number of int | `Off | `On ] -> int = <fun>
# List.map f [`On; `Off];;
- : int list = [1; 0]

[>`Off|`On] list 表示要匹配此列表,您至少應能夠匹配不帶引數的 `Off`On[<`On|`Off|`Number of int] 表示 f 可以應用於不帶引數的 `Off`On,或 `Number n,其中 n 為整數。變體類型內的 >< 表示它們仍可通過定義更多標籤或允許更少標籤來改進。因此,它們包含隱式的類型變數。由於每個變體類型在整個類型中僅出現一次,因此不會顯示其隱式的類型變數。

上面的變體類型是多型的,允許進一步改進。在編寫類型註解時,最常見的是描述固定的變體類型,即無法改進的類型。類型縮寫也是如此。這種類型不包含 <>,而只包含標籤及其關聯類型的枚舉,就像在普通的資料類型定義中一樣。

# type 'a vlist = [`Nil | `Cons of 'a * 'a vlist];;
type 'a vlist = [ `Cons of 'a * 'a vlist | `Nil ]
# let rec map f : 'a vlist -> 'b vlist = function | `Nil -> `Nil | `Cons(a, l) -> `Cons(f a, map f l) ;;
val map : ('a -> 'b) -> 'a vlist -> 'b vlist = <fun>

2 進階用法

多型變體的類型檢查是一件微妙的事情,某些表達式可能會導致更複雜的類型資訊。

# let f = function `A -> `C | `B -> `D | x -> x;;
val f : ([> `A | `B | `C | `D ] as 'a) -> 'a = <fun>
# f `E;;
- : [> `A | `B | `C | `D | `E ] = `E

這裡我們看到了兩種現象。首先,由於此匹配是開放的(最後一個 case 會捕獲任何標籤),我們得到 [> `A | `B] 類型,而不是封閉匹配中的 [< `A | `B] 類型。然後,由於 x 是原樣返回,因此輸入和返回類型相同。as 'a 表示此類型的共享。如果我們將 f 應用於另一個標籤 `E,它將被添加到列表中。

# let f1 = function `A x -> x = 1 | `B -> true | `C -> false let f2 = function `A x -> x = "a" | `B -> true ;;
val f1 : [< `A of int | `B | `C ] -> bool = <fun> val f2 : [< `A of string | `B ] -> bool = <fun>
# let f x = f1 x && f2 x;;
val f : [< `A of string & int | `B ] -> bool = <fun>

這裡 f1f2 都接受變體標籤 `A`B,但 `A 的引數對於 f1int,對於 f2string。在 f 的類型中,只有 f1 接受的 `C 消失了,但是 `A 的兩個引數類型都顯示為 int & string。這表示如果我們將變體標籤 `A 傳遞給 f,其引數應該 同時intstring。由於沒有這樣的值,因此 f 無法應用於 `A,並且 `B 是唯一接受的輸入。

即使數值具有固定的變體類型,仍然可以通過強制轉換賦予其更大的類型。強制轉換通常寫入來源類型和目標類型,但在簡單的情況下,可以省略來源類型。

# type 'a wlist = [`Nil | `Cons of 'a * 'a wlist | `Snoc of 'a wlist * 'a];;
type 'a wlist = [ `Cons of 'a * 'a wlist | `Nil | `Snoc of 'a wlist * 'a ]
# let wlist_of_vlist l = (l : 'a vlist :> 'a wlist);;
val wlist_of_vlist : 'a vlist -> 'a wlist = <fun>
# let open_vlist l = (l : 'a vlist :> [> 'a vlist]);;
val open_vlist : 'a vlist -> [> 'a vlist ] = <fun>
# fun x -> (x :> [`A|`B|`C]);;
- : [< `A | `B | `C ] -> [ `A | `B | `C ] = <fun>

您還可以通過模式匹配來有選擇地強制轉換數值。

# let split_cases = function | `Nil | `Cons _ as x -> `A x | `Snoc _ as x -> `B x ;;
val split_cases : [< `Cons of 'a | `Nil | `Snoc of 'b ] -> [> `A of [> `Cons of 'a | `Nil ] | `B of [> `Snoc of 'b ] ] = <fun>

當由變體標籤組成的或模式被包裹在別名模式內時,該別名會被賦予一個僅包含或模式中枚舉的標籤的類型。這允許許多有用的慣用語,例如函數的增量定義。

# let num x = `Num x let eval1 eval (`Num x) = x let rec eval x = eval1 eval x ;;
val num : 'a -> [> `Num of 'a ] = <fun> val eval1 : 'a -> [< `Num of 'b ] -> 'b = <fun> val eval : [< `Num of 'a ] -> 'a = <fun>
# let plus x y = `Plus(x,y) let eval2 eval = function | `Plus(x,y) -> eval x + eval y | `Num _ as x -> eval1 eval x let rec eval x = eval2 eval x ;;
val plus : 'a -> 'b -> [> `Plus of 'a * 'b ] = <fun> val eval2 : ('a -> int) -> [< `Num of int | `Plus of 'a * 'a ] -> int = <fun> val eval : ([< `Num of int | `Plus of 'a * 'a ] as 'a) -> int = <fun>

為了讓使用上更方便,你可以使用型別定義作為「或」模式的縮寫。也就是說,如果你定義了 type myvariant = [`Tag1 of int | `Tag2 of bool],那麼模式 #myvariant 就等同於寫成 (`Tag1(_ : int) | `Tag2(_ : bool))

這種縮寫可以單獨使用,

# let f = function | #myvariant -> "myvariant" | `Tag3 -> "Tag3";;
val f : [< `Tag1 of int | `Tag2 of bool | `Tag3 ] -> string = <fun>

或者與別名結合使用。

# let g1 = function `Tag1 _ -> "Tag1" | `Tag2 _ -> "Tag2";;
val g1 : [< `Tag1 of 'a | `Tag2 of 'b ] -> string = <fun>
# let g = function | #myvariant as x -> g1 x | `Tag3 -> "Tag3";;
val g : [< `Tag1 of int | `Tag2 of bool | `Tag3 ] -> string = <fun>

3 多型變體的弱點

在了解了多型變體的力量之後,人們可能會想知道,為什麼它們是被加到核心語言變體中,而不是取代它們。

答案有兩方面。首先,雖然多型變體相當有效率,但缺乏靜態型別資訊會導致較少的最佳化,並使多型變體比核心語言變體略重。然而,只有在巨型資料結構上才會出現明顯的差異。

更重要的是,多型變體雖然是型別安全的,但會導致較弱的型別約束。也就是說,核心語言變體實際上不僅確保型別安全,還會檢查你是否只使用了宣告過的建構子,資料結構中存在的所有建構子是否相容,並對其參數強制執行型別約束。

因此,當你使用多型變體時,必須更仔細地明確型別。當你編寫程式庫時,這很容易,因為你可以在介面中描述確切的型別,但對於簡單的程式,你可能最好使用核心語言變體。

還要小心,某些習慣用法會使微小的錯誤很難被發現。例如,下面的程式碼可能出錯,但編譯器無法發現。

# type abc = [`A | `B | `C] ;;
type abc = [ `A | `B | `C ]
# let f = function | `As -> "A" | #abc -> "other" ;;
val f : [< `A | `As | `B | `C ] -> string = <fun>
# let f : abc -> string = f ;;
val f : abc -> string = <fun>

你可以透過註解定義本身來避免此類風險。

# let f : abc -> string = function | `As -> "A" | #abc -> "other" ;;
Error: This pattern matches values of type [? `As ] but a pattern was expected which matches values of type abc The second variant type does not allow tag(s) `As

(章節作者:Jacques Garrigue)