何时应该在OCaml中使用可扩展的变体类型?

Pte*_*mys 7 ocaml

在引入可扩展变体类型之前,我参加了OCaml课程,我对它们了解不多.我有几个问题:

  1. (这个问题被删除了,因为它吸引了"无法回答的客观"投票.)
  2. 使用EVT的低级后果是什么,例如性能,内存表示和(un-)编组?

请注意,我的问题是关于可扩展的变体类型,特别是与建议的相同的问题(在引入EVT之前询问了这个问题!).

oct*_*ron 5

就运行时行为而言,可扩展变体与标准变体有很大不同。

特别是,扩展构造函数是存在于定义它们的模块中的运行时值。例如,在

 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函数因此是安全的:每个函数只能识别自己的扩展构造函数。