OCaml 中的 Phantom Type 是什么

Info

= 左侧是类型(Type),右侧是值(Value)

只需要在定义 Variant Type 的时候,在 = 左侧加上 'a 等泛型变量,但是在 = 不要放,像下面这样

ocaml

type 'a t = some_type

这里的精髓在于:= 右侧并没有使用到 'a,因此 'a 其实我们想放什么都可以

这里的 'a 就是 Phantom Type,它出现在类型(= 左侧)当中,但是在创建值的时候并不会用到,所以说是 Phantom (幽灵👻)

既然没有用到,那么干脆就不要写 'a,而是直接用 _,这是比较 idiomatic 的写法

ocaml

type _ t = some_type

接下来我们看看如何使用 Phantom Types

Info

在我看来,Phantom type 最大的用处是给类型加上“状态”,因此可以进行更细粒度的类型区分

我这里就借用 Haskell MOOC 里面的例子:货币计算 1。显然,不管是什么货币类型,都可以用一个浮点数表示(在 OCaml 里面对应 float 类型)。让我们用 Phantom Type 定义如下的货币类型

ocaml

utop # type _ currency = Currency of float;;

然后定义 2 个 Phantom Type:美元和人民币

ocaml

utop # type dollar;;
utop # type yuan;;

现在来写一个货币相加的函数,假设我们只想要让美元相加,那么我们可以这么写

ocaml

utop # let dollar_add (Currency x : dollar currency) (Currency y : dollar currency) :
    dollar currency =
  Currency (x +. y);;

val dollar_add : dollar currency -> dollar currency -> dollar currency = <fun>

从函数签名可以看到,这个函数只会让美元相加,我们可以尝试验证一下

ocaml

utop # let three_dollar : dollar currency = Currency 3.0;;
utop # let four_dollar : dollar currency = Currency 4.0;;

utop # let four_dollar : dollar currency = Currency 4.0;;
val four_dollar : dollar currency = Currency 4.

可以看到,美元求和没有问题,可以试一下用这个函数对人民币求和

ocaml

utop # let yuan_sum = dollar_add three_yuan four_yuan;;
Error: This expression has type yuan currency
       but an expression was expected of type dollar currency
       Type yuan is not compatible with type dollar

在编译期间,编译器帮我们捕捉到了这个错误😺

但如果我们想要一个通用的函数可以处理任何货币类型呢?我们可以选择加上 'a currency 这种类型声明,比如下面这个 scale 函数

ocaml

utop # let scale (Currency x : 'a currency) factor : 'a currency =
    Currency (x *. factor);;

val scale : 'a currency -> float -> 'a currency = <fun>

也可以选择省略掉类型声明

ocaml

utop # let scale (Currency x) factor = Currency (x *. factor);;

val scale : 'a currency -> float -> 'b currency = <fun>

不一样的是函数返回类型变成了 'b currency,这是因为 OCaml 总是会推断更 General 的情况。如果这不是你的预期行为,还是得自己加上类型声明

和不使用 Phantom Type 相比,使用 Phantom Type 的好处是什么?在我看来有如下几点

  1. 扩展性:每当我们要添加一个新的 case 的时候,只需要多写一个 Phantom Type(type ...)即可,扩展性比较好
  2. 类型安全:通过给函数加上类型声明,就可以控制函数的行为,实现更细粒度的控制。OCaml 编译器会帮我们追踪“状态”,在编译期间就能够捕捉类型错误

因为函数要加上类型声明,使用 Phantom Type 之后代码似乎更加啰嗦了。但实际上,写 OCaml 代码的时候可以选择将类型声明放在单独的 .mli 文件里面,在对应的 .ml 文件里面写实现,这样代码看上去仍然是比较简洁的。本文受限于篇幅并没有采取这种方式。可以在 Jane Street 的 这篇博客 里面看到这种用法