为什么附录中给出的两个示例的类型不同的问题让我困惑了几天。这是我通过深入研究 OCaml 编译器的代码发现的(免责声明:我既不是 OCaml 方面的专家,也不是 ML 类型系统方面的专家)。
回顾
# let _x = 3 in (fun () -> ref None);; (* (1) *)
- : unit -> 'a option ref = <fun>
被赋予多态类型 (think ∀ α. unit → α option ref
) 而
# let _x = ref 3 in (fun () -> ref None);; (* (2) *)
- : unit -> '_a option ref = <fun>
给定一个单态类型(想想unit → α option ref
,即类型变量α
不是全称量化的)。
直觉
出于类型检查的目的,OCaml 编译器认为示例 (2) 和
# let r = ref None in (fun () -> r);; (* (3) *)
- : unit -> '_a option ref = <fun>
因为它不会查看 的主体let
以查看是否实际使用了绑定变量(正如人们所期望的那样)。但是(3)显然必须给定一个单态类型,否则多态类型的参考单元可能会逃逸,可能会导致不健全的行为,如内存损坏。
扩展性
为了理解为什么 (1) 和 (2) 是这样输入的,让我们看看 OCaml 编译器实际上是如何检查一个let
表达式是否是一个值(即“非扩展”)(参见is_nonexpansive):
let rec is_nonexpansive exp =
match exp.exp_desc with
(* ... *)
| Texp_let(rec_flag, pat_exp_list, body) ->
List.for_all (fun vb -> is_nonexpansive vb.vb_expr) pat_exp_list &&
is_nonexpansive body
| (* ... *)
因此,let
如果 -expression 的主体和所有绑定变量都是值,则它就是一个值。
在附录中给出的两个示例中,主体都是fun () -> ref None
,它是一个函数,因此是一个值。两段代码之间的区别在于它3
是一个值,而ref 3
不是。因此 OCaml 认为第let
一个是一个值,而不是第二个。
打字
再次查看 OCaml 编译器的代码,我们可以看到一个表达式是否被认为是可扩展的,这决定了 - 表达式的类型是如何let
泛化的(参见type_expression):
(* Typing of toplevel expressions *)
let type_expression env sexp =
(* ... *)
let exp = type_exp env sexp in
(* ... *)
if is_nonexpansive exp then generalize exp.exp_type
else generalize_expansive env exp.exp_type;
(* ... *)
由于let _x = 3 in (fun () -> ref None)
它是非扩展性的,因此使用它进行类型化,generalize
从而赋予它多态类型。let _x = ref 3 in (fun () -> ref None)
另一方面,通过 输入generalize_expansive
,给它一个单态类型。
这就是我所得到的。如果您想更深入地挖掘,请同时阅读 Oleg Kiselyov 的Efficient and Insightful Generalizationgeneralize
,generalize_expansive
这可能是一个好的开始。
非常感谢剑桥 OCaml Labs 的 Leo White 鼓励我开始挖掘!