Encoding 可推断记录的编码
您可能知道,ocaml中的记录有些特殊,因为每个标签必须唯一地分配给一个标称记录类型,即,如果没有上下文,则无法键入以下函数:Encoding 可推断记录的编码,encoding,ocaml,record,Encoding,Ocaml,Record,您可能知道,ocaml中的记录有些特殊,因为每个标签必须唯一地分配给一个标称记录类型,即,如果没有上下文,则无法键入以下函数: let f r = r.x 适当的第一类记录(即行为类似于带标签的元组的东西)使用对象进行简单编码,例如 let f r = r#x 当以正确的方式创建对象时(即没有自递归,没有变异),它们的行为就像记录一样 然而,我对这个解决方案有点不满意,原因有两个: 当使记录可更新时(即通过为每个标签l添加一个显式的“with_l”方法),类型有点过于松散(它应该与原始记录
let f r = r.x
适当的第一类记录(即行为类似于带标签的元组的东西)使用对象进行简单编码,例如
let f r = r#x
当以正确的方式创建对象时(即没有自递归,没有变异),它们的行为就像记录一样
然而,我对这个解决方案有点不满意,原因有两个:
这种编码能避免上面提到的问题吗?如果我理解正确,您正在寻找一种非常特殊的多态性。您希望编写一个适用于所有类型的函数,以便该类型是具有特定字段的记录。这听起来更像是C++风格中的语法多态性,而不是ML风格的语义多态性。如果我们稍微改变一下任务的措辞,通过理解字段访问只是字段投影函数的语法糖分,那么我们可以说,您想要编写一个在提供特定操作集的所有类型上都是多态的函数。OCaml可以使用以下机制之一捕获这种多态性:
print\u student
,它将处理满足student
签名的任何类型:
module type Student = sig
type t
val name : t -> string
val age : t -> int
end
let print_student (type t)
(module S : Student with type t = t) (s : t) =
Printf.printf "%s %d" (S.name s) (S.age s)
print_student
函数的类型是(类型为t='a)->'a->unit
。因此,它适用于满足Student
接口的任何类型,因此它是多态的。这是一个非常强大的多态性,需要付出代价,在调用函数时需要显式地传递模块结构,因此它是System F风格的多态性。functor还要求您指定具体的模块结构。因此,这两种类型都不是可推断的(即,不是您正在寻找的隐式Hindley-Milner样式多态性)。对于后者,只有对象可以工作(也有模块化隐式,它放松了明确性要求,但它们仍然不在主干中,但它们实际上会满足您的要求)
使用对象样式行多态性,可以编写一个函数,该函数在符合某个签名的一组类型上具有多态性,并且可以从函数定义隐式推断该签名。然而,这种力量是有代价的。因为对象操作是用方法编码的,而方法只是在运行时动态分配的函数指针,所以不应该期望任何编译时优化。不可能对动态绑定的对象执行任何静态分析。当然,没有公共子表达式消除,也没有内联。对于函子和一级模块,可以使用flamba
(参见4.03.0+flambda
opam开关)在较新的编译器分支上进行优化。但是在常规的编译器安装中,不会执行内联
不同的方法
关于其他技术呢。首先,我们可以使用camlp{4,5}
,或ppx
,甚至m4
和cpp
对代码进行预处理,但这几乎不是惯用的方法,而且用处不大
另一种方法是,我们可以尝试找到合适的单态数据类型,而不是编写多态函数。直接方法是使用多态性变体列表,例如
type attributes = [`name of string | `age of int]
type student = attribute list
事实上,我们甚至不需要预先指定所有这些类型,我们的函数只需要那些需要的字段,这是行多态的一种形式:
let rec name = function
| [] -> raise Not_found
| `name n -> n
| _ :: student -> name student
这种编码的唯一问题是,您不能保证相同的命名属性可以出现一次,而且只能出现一次。所以有可能一个学生根本没有名字,或者,更糟糕的是,他可以有不止一个名字。根据您的问题领域,这是可以接受的
如果不是,那么我们可以使用GADT和可扩展变体来编码异构映射,即,将键映射到的关联数据结构
不同的类型(在常规(同质)映射或关联列表中,值类型是统一的)。如何构造这样的容器超出了答案的范围,但幸运的是,至少有两个可用的实现。一个是我个人使用的通用地图(Univ\u-map
),由Core
(实际上是Core\u-kernel
)提供。它允许您指定两种异构映射,使用默认值和不使用默认值。前者对应一个带有可选字段的记录,后者对每个字段都有默认值,因此访问器是一个total函数。比如说,
open Core_kernel.Std
module Dict = Univ_map.With_default
let name = Dict.Key.create ~name:"name" ~default:"Joe" sexp_of_string
let age = Dict.Key.create ~name:"age" ~default:18 sexp_of_int
let print student =
printf "%s %d"
(Dict.get student name) (Dict.get age name)
<x : int; y : float> Poly_record.t
您可以使用抽象类型隐藏您正在使用的通用映射,因为只有一个Dict.t
可以跨不同的抽象使用,这可能会破坏模块化。异构映射实现的另一个示例来自。它不提供带有默认映射的,但依赖性要小得多
当然,对于这种多余的情况,如果
(** [copy_with r [(k1,v1);..;(kn,vn)]] is poly-record version of
[{r with k1 = v1; ..; kn = vn}] *)
let copy_with r fields =
let r = Hashtbl.copy r in
List.iter (fun (k,v) -> Hashtbl.replace r (Btype.hash_variant k) v) fields
(** [create [(k1,v1);..(kn,vn)]] is poly-record version of [{k1=v1;..;kn=vn}] *)
let create fields = copy_with fields (Hashtbl.create (List.length fields))
<x : int; y : float> Poly_record.t
function `l_1 k -> k v_1 | `l_2 k -> k v_2 | ... | `l_n k -> k v_n
# let myRecord = (function `field1 k -> k 123 | `field2 k -> k "hello") ;;
(* encodes { field1 = 123; field2 = "hello" } *)
val myRecord : [< `field1 of int -> 'a | `field2 of string -> 'a ] -> 'a = <fun>
# let getField1 r = r (`field1 (fun v -> v)) ;;
(* fun r -> r.field1 *)
val getField1 : ([> `field1 of 'a -> 'a ] -> 'b) -> 'b = <fun>
# getField1 myRecord ;;
- : int = 123
# let getField2 r = r (`field2 (fun v -> v)) ;;
(* fun r -> r.field2 *)
val getField2 : ([> `field2 of 'a -> 'a ] -> 'b) -> 'b = <fun>
# getField2 myRecord ;;
- : string = "hello"
let ref1 = ref 123
let ref2 = ref "hello"
let myRecord =
function
| `field1 k -> k !ref1
| `field2 k -> k !ref2
| `set_field1(v1, k) -> k (ref1 := v1)
| `set_field2(v2, k) -> k (ref2 := v2)
let myRecord =
let ref1 = ref 123 in
let ref2 = ref "hello" in
function
| `field1 k -> k !ref1
| `field2 k -> k !ref2
| `set_field1(v1, k) -> k (ref1 := v1)
| `set_field2(v2, k) -> k (ref2 := v2)