使用 OCaml 中的 Polymorphic Variant 类型
引言
我已经学习并使用了 OCaml 有段时间了,但是一直搞不清楚 Polymorphic variant 有什么用。最近在看 Yojson 的时候又看到了这种用法,一番搜索之后发现并没有看到关于 Polymorphic variant 的比较好的文章(官方介绍 在我看来有点难懂),只看到一些相关的回答12。经过仔细学习之后,我决定写一篇文章,希望能对你有所帮助 :)
语法
用方括号 []
括起来,每一个 constructor 用 backtick ( ` )标识,比如
type either = [
| `A of int
| `B
]
在定义值的时候,也要加上 backtick
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,比如
[`A of int | `B]
[`A of int | `B | `C]
...
因此,polymorphic variant 也叫做 open variant
同理有 [< ...]
,这个类型约束的意思是:该类型最多有这些 constructor,可以更少但不能多。比如
[< `A of int | `B]
那么可以有如下 3 种情况
[`A of int]
[`B]
[`A of int | `B]
Pattern Matching
如果函数入参是一个 polymorphic variant,我们可以尝试写一下 Pattern Matching 的函数,比如
utop # let foo = function
| `A -> "A"
| `B -> "B" ;;
val foo : [< `A | `B ] -> string/2 = <fun>
可以看到,OCaml 推断的入参类型是 [< ...]
,这是合理的,因为 如果我们提供了其他的 constructor,这个函数是无法处理的,比如
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
函数
utop # let bar = function
| `A -> "A"
| `C -> "C"
val bar : [< `A | `C ] -> string/2 = <fun>
然后我们可以尝试将 foo
函数和 bar
函数放在同一个列表里面,那么这个函数列表应该有什么类型?我们可以尝试自己做一下类型推断,已知
foo
函数可以处理A
或者是B
bar
函数可以处理A
或者是C
- 一个列表要求里面所有的 item 的类型是一样的
那么函数列表的类型就应该是 ([< A] -> string) list
,可以用 utop
可以检查一下是否如此
utop # let funcs = [ foo; bar ];;
val funcs : ([< `A ] -> string/2) list/2 = [<fun>; <fun>]
[> ...]
和 [< ...]
的使用情况自动进行类型推断,找到符合的类型另外,在做 Pattern Matching 的时候,可以用 #some_variant
的语法表示整个 polymorphic variant,比如
type my_type =
[ `A
| `B
]
let process_a_b_c = function
| #my_type -> "A or B"
| `C -> "C"
使用场景
不定义直接使用
这是最直观的应用,有时候只是在某一个地方用到了 Variant Type,用 type name = ...
定义一下会显得有点多余,这种时候直接用 Polymorphic Variant Type
下面的例子可以用来对比一下
type return_type =
| A of int
| B
val f : int -> return_val
使用 Polymorphic Variant 你可以这么写,就不用多定义一个类型了
val f : int -> [ `A of int | `B ]
共享数据类型
在使用某个库的时候,你可能需要用到它的定义的数据类型,这个时候你可以将这个库列为你的依赖,比如
(* ./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 定义的,那么就可以消去这个依赖
(* ./mycolor.ml *)
type rgb = [ `Red | `Green | `Blue ]
(* ./main.ml *)
let process_color = function
| `Red -> "Red" (* the dependence is gone *)
| `Green -> "Green"
| `Blue -> "Blue"
Subtype
Polymorphic variant 特别适合不同的 Variant type 有共同的部分这种情况,在写处理这些类型的函数的时候很方便
举例来说,common
是 foo
和 bar
的子类型,因为 foo
和 bar
都包含 A
和 B
这两个 constructor
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 个函数,如下所示
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,上面的代码就可以简化为
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
- Complexity:Polymorphic Variants 的 typing rule 更加复杂,特别是报错信息更不友好了
- Error-finding:用 Polymorphic Variants 会导致你不大容易找到 Bug
- Efficiency:性能会稍微差一些