OCaml 中的 Phantom Type 是什么
语法
=
左侧是类型(Type),右侧是值(Value)
只需要在定义 Variant Type 的时候,在 =
左侧加上 'a
等泛型变量,但是在 =
不要放,像下面这样
type 'a t = some_type
这里的精髓在于:=
右侧并没有使用到 'a
,因此 'a
其实我们想放什么都可以
这里的 'a
就是 Phantom Type,它出现在类型(=
左侧)当中,但是在创建值的时候并不会用到,所以说是 Phantom (幽灵👻)
既然没有用到,那么干脆就不要写 'a
,而是直接用 _
,这是比较 idiomatic 的写法
type _ t = some_type
接下来我们看看如何使用 Phantom Types
使用场合
在我看来,Phantom type 最大的用处是给类型加上“状态”,因此可以进行更细粒度的类型区分
我这里就借用 Haskell MOOC 里面的例子:货币计算 1。显然,不管是什么货币类型,都可以用一个浮点数表示(在 OCaml 里面对应 float
类型)。让我们用 Phantom Type 定义如下的货币类型
utop # type _ currency = Currency of float;;
然后定义 2 个 Phantom Type:美元和人民币
utop # type dollar;;
utop # type yuan;;
现在来写一个货币相加的函数,假设我们只想要让美元相加,那么我们可以这么写
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>
从函数签名可以看到,这个函数只会让美元相加,我们可以尝试验证一下
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.
可以看到,美元求和没有问题,可以试一下用这个函数对人民币求和
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
函数
utop # let scale (Currency x : 'a currency) factor : 'a currency =
Currency (x *. factor);;
val scale : 'a currency -> float -> 'a currency = <fun>
也可以选择省略掉类型声明
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 的好处是什么?在我看来有如下几点
- 扩展性:每当我们要添加一个新的 case 的时候,只需要多写一个 Phantom Type(
type ...
)即可,扩展性比较好 - 类型安全:通过给函数加上类型声明,就可以控制函数的行为,实现更细粒度的控制。OCaml 编译器会帮我们追踪“状态”,在编译期间就能够捕捉类型错误
因为函数要加上类型声明,使用 Phantom Type 之后代码似乎更加啰嗦了。但实际上,写 OCaml 代码的时候可以选择将类型声明放在单独的 .mli
文件里面,在对应的 .ml
文件里面写实现,这样代码看上去仍然是比较简洁的。本文受限于篇幅并没有采取这种方式。可以在 Jane Street 的 这篇博客 里面看到这种用法