在引入可扩展变体类型之前,我参加了OCaml课程,我对它们了解不多.我有几个问题:
请注意,我的问题是关于可扩展的变体类型,特别是与建议的相同的问题(在引入EVT之前询问了这个问题!).
就运行时行为而言,可扩展变体与标准变体有很大不同。
特别是,扩展构造函数是存在于定义它们的模块中的运行时值。例如,在
type t = ..
module M = struct
type t +=A
end
open M
Run Code Online (Sandbox Code Playgroud)
第二行定义一个新的扩展构造函数值A并将其添加到M运行时的现有扩展构造函数中。相反,经典变体在运行时并不真正存在。
通过注意到我可以对经典变体使用仅 mli 编译单元,可以观察到这种差异:
(* classical.mli *)
type t = A
(* main.ml *)
let x = Classical.A
Run Code Online (Sandbox Code Playgroud)
然后编译main.ml与
ocamlopt classic.mli main.ml
没有麻烦,因为Classical模块中不涉及任何价值。
与可扩展变体相反,这是不可能的。如果我有
(* ext.mli *)
type t = ..
type t+=A
(* main.ml *)
let x = Ext.A
Run Code Online (Sandbox Code Playgroud)
然后命令
ocamlopt ext.mli main.ml
失败
错误:所需的模块“Ext”不可用
因为Ext.A缺少扩展构造函数的运行时值。
您还可以使用Obj模块查看扩展构造函数的名称和 id以查看这些值
let a = [%extension_constructor A]
Obj.extension_name a;;
Run Code Online (Sandbox Code Playgroud)
- :字符串=“MA”
Obj.extension_id a;;
Run Code Online (Sandbox Code Playgroud)
- : 整数 = 144
(这id是非常脆弱的,它的价值没有特别的意义。)重要的一点是扩展构造函数是通过它们的内存位置来区分的。因此,带n参数的构造函数被实现为带n+1参数的块,其中第一个隐藏参数是扩展构造函数:
type t += B of int
let x = B 0;;
Run Code Online (Sandbox Code Playgroud)
这里,x包含两个字段,而不是一个:
Obj.size (Obj.repr x);;
Run Code Online (Sandbox Code Playgroud)
- : 整数 = 2
第一个字段是扩展构造函数B:
Obj.field (Obj.repr x) 0 == Obj.repr [%extension_constructor B];;
Run Code Online (Sandbox Code Playgroud)
- :布尔=真
前面的语句也适用于n=0:与经典变体相反,可扩展变体永远不会表示为标记整数。
由于编组不保持物理平等,这意味着不能在不失去身份的情况下编组可扩展和类型。例如,与
let round_trip (x:'a):'a = Marshall.from_string (Marshall.to_string x []) 0
Run Code Online (Sandbox Code Playgroud)
然后测试结果
type t += C
let is_c = function
| C -> true
| _ -> false
Run Code Online (Sandbox Code Playgroud)
导致失败:
is_c (round_trip C)
Run Code Online (Sandbox Code Playgroud)
- :布尔=假
因为在读取编组值时往返分配了一个新块 这与异常已经存在的问题相同,因为异常是可扩展的变体。
这也意味着可扩展类型的模式匹配在运行时完全不同。例如,如果我定义一个简单的变体
type s = A of int | B of int
Run Code Online (Sandbox Code Playgroud)
并将函数定义f为
let f = function
| A n | B n -> n
Run Code Online (Sandbox Code Playgroud)
编译器足够聪明,可以优化此函数以简单地访问参数的第一个字段。
您可以检查ocamlc -dlambda上面的函数是否在 Lambda 中介表示中表示为:
(function param/1008 (field 0 param/1008)))
Run Code Online (Sandbox Code Playgroud)
然而,对于可扩展的变体,我们不仅需要一个默认模式
type e = ..
type e += A of n | B of n
let g = function
| A n | B n -> n
| _ -> 0
Run Code Online (Sandbox Code Playgroud)
但是我们还需要将参数与匹配中的每个扩展构造函数进行比较,从而导致匹配的更复杂的 lambda IR
(function param/1009
(catch
(if (== (field 0 param/1009) A/1003) (exit 1 (field 1 param/1009))
(if (== (field 0 param/1009) B/1004) (exit 1 (field 1 param/1009))
0))
with (1 n/1007) n/1007)))
Run Code Online (Sandbox Code Playgroud)
最后,以可扩展变体的实际示例作为结论,在 OCaml 4.08 中,格式模块将其基于字符串的用户定义标签替换为可扩展变体。
这意味着定义新标签如下所示:
首先,我们从新标签的实际定义开始
type t = Format.stag = ..
type Format.stag += Warning | Error
Run Code Online (Sandbox Code Playgroud)
那么这些新标签的翻译函数是
let mark_open_stag tag =
match tag with
| Error -> "\x1b[31m" (* aka print the content of the tag in red *)
| Warning -> "\x1b[35m" (* ... in purple *)
| _ -> ""
let mark_close_stag _tag =
"\x1b[0m" (*reset *)
Run Code Online (Sandbox Code Playgroud)
然后安装新标签
let enable ppf =
Format.pp_set_tags ppf true;
Format.pp_set_mark_tags ppf true;
Format.pp_set_formatter_stag_functions ppf
{ (Format.pp_get_formatter_stag_functions ppf ()) with
mark_open_stag; mark_close_stag }
Run Code Online (Sandbox Code Playgroud)
使用一些辅助功能,可以使用这些新标签进行打印
Format.printf "This message is %a.@." error "important"
Format.printf "This one %a.@." warning "not so much"
Run Code Online (Sandbox Code Playgroud)
与字符串标签相比,有以下几个优点:
mark_open_stag函数因此是安全的:每个函数只能识别自己的扩展构造函数。