使用 OCaml 中的 Polymorphic Variant 类型

我已经学习并使用了 OCaml 有段时间了,但是一直搞不清楚 Polymorphic variant 有什么用。最近在看 Yojson 的时候又看到了这种用法,一番搜索之后发现并没有看到关于 Polymorphic variant 的比较好的文章(官方介绍 在我看来有点难懂),只看到一些相关的回答12。经过仔细学习之后,我决定写一篇文章,希望能对你有所帮助 :)

信息
不是一个 OCaml 专家,所以本文可能也不是一篇比较好的文章

用方括号 [] 括起来,每一个 constructor 用 backtick ( ` )标识,比如

ocaml

type either = [
  | `A of int
  | `B
]

在定义值的时候,也要加上 backtick

ocaml

utop # let v = `A 1;;
val v : [> `A of int/2 ] = `A 1
技巧
int/2 的意思可以看 StackOverflow 上的一个 回答

前面我们定义 v 的可以看到,类型并不是 either,而是 [> ...],这是什么意思?根据 polymorphic variant 的 syntax,我们知道 [ ... ] 是 polymorphic variant type,那 > 呢?

它跟数学中的 > 有类似的语义,表示除了我们指定的 constructor 以外还可以有更多的 constructor,比如

ocaml

[`A of int | `B]
[`A of int | `B | `C]
...

因此,polymorphic variant 也叫做 open variant


同理有 [< ...],这个类型约束的意思是:该类型最多有这些 constructor,可以更少但不能多。比如

ocaml

[< `A of int | `B]

那么可以有如下 3 种情况

ocaml

[`A of int]
[`B]
[`A of int | `B]

如果函数入参是一个 polymorphic variant,我们可以尝试写一下 Pattern Matching 的函数,比如

ocaml

utop # let foo = function
  | `A -> "A"
  | `B -> "B" ;;
  
val foo : [< `A | `B ] -> string/2 = <fun>

可以看到,OCaml 推断的入参类型是 [< ...],这是合理的,因为 如果我们提供了其他的 constructor,这个函数是无法处理的,比如

ocaml

utop # foo `C;;
Error: This expression has type [> `C ] but an expression was expected of type
         [< `A | `B ]
       The second variant type does not allow tag(s) `C

现在我们对 foo 函数稍作修改,将 B 的分支改成 C,得到 bar 函数

ocaml

utop # let bar = function
  | `A -> "A"
  | `C -> "C"
  
val bar : [< `A | `C ] -> string/2 = <fun>

然后我们可以尝试将 foo 函数和 bar 函数放在同一个列表里面,那么这个函数列表应该有什么类型?我们可以尝试自己做一下类型推断,已知

  • foo 函数可以处理 A 或者是 B
  • bar 函数可以处理 A 或者是 C
  • 一个列表要求里面所有的 item 的类型是一样的
警告
你可能看到我在写 Inline Code 的时候省略了 backtick,这是因为会和 Markdown 的 Inline Code 语法冲突

那么函数列表的类型就应该是 ([< A] -> string) list,可以用 utop 可以检查一下是否如此

ocaml

utop # let funcs = [ foo; bar ];;
val funcs : ([< `A ] -> string/2) list/2 = [<fun>; <fun>]
信息
通过前面的例子,我们发现,OCaml 可以根据 [> ...][< ...] 的使用情况自动进行类型推断,找到符合的类型

另外,在做 Pattern Matching 的时候,可以用 #some_variant 的语法表示整个 polymorphic variant,比如

ocaml

type my_type =
  [ `A
  | `B
  ]

let process_a_b_c = function
  | #my_type -> "A or B"
  | `C -> "C"
信息
更多的使用场景可以参考论文 Programming with Polymorphic Variants

这是最直观的应用,有时候只是在某一个地方用到了 Variant Type,用 type name = ... 定义一下会显得有点多余,这种时候直接用 Polymorphic Variant Type

下面的例子可以用来对比一下

ocaml

type return_type =
  | A of int
  | B

val f : int -> return_val

使用 Polymorphic Variant 你可以这么写,就不用多定义一个类型了

ocaml

val f : int -> [ `A of int | `B ]

在使用某个库的时候,你可能需要用到它的定义的数据类型,这个时候你可以将这个库列为你的依赖,比如

ocaml

(* ./mycolor.ml *)
type rgb = Red | Green | Blue

(* ./main.ml *)
let process_color = function
  | Mycolor.Red -> "Red"    (* the Main module depends on Mycolor *)
  | Mycolor.Green -> "Green"
  | Mycolor.Blue -> "Blue"

但如果数据类型是用 Polymorphic Variant 定义的,那么就可以消去这个依赖

ocaml

(* ./mycolor.ml *)
type rgb = [ `Red | `Green | `Blue ]
  
(* ./main.ml *)
let process_color = function
  | `Red -> "Red"    (* the dependence is gone *)
  | `Green -> "Green"
  | `Blue -> "Blue"
技巧
通常这个时候,库的设计者会在文档里面告诉你,比如 Yojson 的 JSON 类型 就是用 polymorphic variant 定义的

Polymorphic variant 特别适合不同的 Variant type 有共同的部分这种情况,在写处理这些类型的函数的时候很方便

举例来说,commonfoobar 的子类型,因为 foobar 都包含 AB 这两个 constructor

ocaml

type common = A | B

type foo =
  | Common of common (* it's kind of redundant *)
  | C
  | D

type bar =
  | Common of common
  | E
  | F

现在假设有一个 show 函数,需要处理上面 3 种类型,那么你不得不写 3 个函数,如下所示

ocaml

let show = function
  | A -> "A"
  | B -> "B"

let show_foo arg =
  match (arg : foo) with
  | Common v -> show v
  | C -> "C"
  | D -> "D"

let show_bar arg =
  match (arg : bar) with
  | Common v -> show v
  | E -> "E"
  | F -> "F"

除此之外,我们还需要加上 : foo: bar 来区分不同的类型。而如果使用 Polymorphic variant,上面的代码就可以简化为

ocaml

type common = [ `A | `B ]

type foo = [ common | `C | `D ]

type bar = [ common | `E | `F ]

let show = function
  | `A -> "A"
  | `B -> "B"
  | `C -> "C"
  | `D -> "D"
  | `E -> "E"
  | `F -> "F"

尽管 polymorphic variant 用起来似乎更加灵活和方便,但这是有代价的 3

  1. Complexity:Polymorphic Variants 的 typing rule 更加复杂,特别是报错信息更不友好了
  2. Error-finding:用 Polymorphic Variants 会导致你不大容易找到 Bug
  3. Efficiency:性能会稍微差一些