第 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 是一个整数。变体类型中的 >< 表明它们仍然可以被细化,要么通过定义更多标签,要么通过允许更少标签。因此,它们包含一个隐式类型变量。由于每个变体类型在整个类型中只出现一次,因此它们的隐式类型变量不会显示。

上述变体类型是多态的,允许进一步细化。在编写类型注释时,人们通常会描述固定变体类型,即不能细化的类型。类型缩写也是如此。这种类型不包含 <>,而只包含标签及其关联类型的枚举,就像在普通的datatype定义中一样。

# 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

这里我们看到了两种现象。首先,由于这种匹配是开放的(最后一个情况捕获任何标签),我们得到了类型 [> `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 形式出现。这意味着,如果我们传递变体标签 `Af,它的参数应该同时是 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: 此模式匹配类型为 [? `As ] 的值,但期望的模式是匹配类型为 abc 的值。第二个变体类型不允许标签 `As

(章节由 Jacques Garrigue 撰写)