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

时间:2019-02-17 05:21:51

标签: ocaml

在介绍extensible variant types之前,我参加了有关OCaml的课程,但我对它们并不了解。我有几个问题:

  1. (此问题被删除,因为它引起了“客观上不负责回答”的密切投票。)
  2. 使用EVT的低级后果是什么,例如性能,内存表示和(非)封送处理?

请注意,我的问题具体是关于可扩展变量类型的,与建议的与此问题相同的问题不同(该问题是在引入EVT之前提出的!)。

1 个答案:

答案 0 :(得分:1)

可扩展变体与标准变体的区别在于 运行时行为。

尤其是,扩展构造函数是驻留在内部的运行时值 定义它们的模块。例如,在

 type t = ..
 module M = struct
   type t +=A
 end
 open M

第二行定义新的扩展构造函数值A并将其添加到 M的现有扩展构造函数在运行时。 相反,经典变体在运行时并不真正存在。

注意到我可以使用,可以观察到这种差异 一个仅mli的经典变体编译单元:

 (* classical.mli *)
 type t = A

 (* main.ml *)
 let x = Classical.A

然后使用{p>编译main.ml

  

ocamlopt classic.mli main.ml

没有麻烦,因为Classical模块中没有价值。

与可扩展的变体相反,这是不可能的。如果有

 (* ext.mli *)
  type t = ..
  type t+=A

 (* main.ml *)
 let x = Ext.A

然后输入命令

  

ocamlopt ext.mli main.ml

失败

  

错误:必需的模块“ Ext”不可用

因为缺少扩展构造函数Ext.A的运行时值。

您还可以同时查看扩展构造函数的名称和ID 使用Obj模块查看这些值

 let a = [%extension_constructor A]
 Obj.extension_name a;;
  
      
  • :字符串=“ M.A”
  •   
 Obj.extension_id a;;
  
      
  • :int = 144
  •   

(此id十分脆弱,其价值也不是特别有意义。) 重要的一点是,扩展构造函数使用它们的区别 内存位置。因此,实现了带有n参数的构造函数 作为带有n+1参数的块,其中第一个隐藏参数是扩展名 构造函数:

type t += B of int
let x = B 0;;

在这里,x包含两个字段,而不是一个字段:

 Obj.size (Obj.repr x);;
  
      
  • :int = 2
  •   

第一个字段是扩展构造函数B

 Obj.field (Obj.repr x) 0 == Obj.repr [%extension_constructor B];;
  
      
  • :bool = true
  •   

上一条语句也适用于n=0:可扩展的变体永远不会 表示为带标记的整数,与经典变体相反。

由于编组不能保持物理上的平等,因此意味着可扩展 总和类型不能在不丢失其身份的情况下进行编组。例如,

 let round_trip (x:'a):'a = Marshall.from_string (Marshall.to_string x []) 0

然后使用

测试结果
  type t += C
  let is_c = function
  | C -> true
  | _ -> false

导致失败:

   is_c (round_trip C)
  
      
  • :bool = false
  •   

因为在读取编组值时往返分配了一个新块 这与异常已经存在的问题相同,因为异常 是可扩展的变体。

这也意味着可扩展类型的模式匹配有很大不同 在运行时。例如,如果我定义一个简单的变体

 type s = A of int | B of int

并将函数f定义为

let f = function
| A n | B n -> n

编译器足够聪明,可以优化此功能,从而只需访问 参数的第一个字段。

您可以使用ocamlc -dlambda检查上面的函数是否表示为 Lambda中介表示为:

(function param/1008 (field 0 param/1008)))

但是,对于可扩展的变体,我们不仅需要默认模式

   type e = ..
   type e += A of n | B of n
   let g = function
   | A n | B n -> n
   | _ -> 0

,但我们还需要将参数与 比赛导致比赛的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)))

最后,以可扩展变体的实际示例作为结束, 在OCaml 4.08中,格式模块替换了其基于字符串的用户定义标签 具有可扩展的变体。

这意味着定义新标签看起来像这样:

首先,我们从新标签的实际定义开始

 type t =  Format.stag = ..
 type Format.stag += Warning | Error

然后这些新标签的翻译功能是

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 *)

安装新标签的步骤如下

 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 }

通过一些帮助功能,可以使用这些新标签进行打印

 Format.printf "This message is %a.@." error "important"
 Format.printf "This one %a.@." warning "not so much"

与字符串标签相比,优点不多:

  • 没有拼写错误的空间
  • 无需序列化/反序列化潜在的复杂数据
  • 在名称相同的不同扩展构造函数之间不要混淆。
  • 因此,
  • 链接多个用户定义的mark_open_stag函数是安全的: 每个函数只能识别自己的扩展构造函数。