修复记录类型具有相同标签时的类型推断

Fixing type inference when record types have same labels

考虑以下记录类型:

type t1 = {
    label1: string option
  ; label2: (string * int) list
  ; label3: (string * int) list
  ; }

type t2 = {
    label1: string option
  ; label2: (string * int) list
  ; label3: ((string * int) list) list
  ; }

我知道我不能在同一模块中定义类型 t1t2,因为 label1label2 具有相同的类型,并且在 t1t2 中都有定义,这将中断类型推断。因此,如果更改标签是不可能的,我有两个选择:

我可以使用模块命名空间:

module M1 = struct
    type t1 = {
      label1: string option
     ; label2: (string * int) list
     ; label3: (string * int) list
     ; }
end

module M2 = struct
   type t2 = {
       label1: string option
    ; label2: (string * int) list
    ; label3: ((string * int) list) list
    ; }
end

或者我可以在同一文件中定义的每个函数中指定类型:

let f1 (param: t1) = ...

let f2 (param: t2) = ...

let f3 (param: t1) = ...

每种方法的优缺点是什么?

首先,它不会破坏类型推断,您可以在同一模块中定义具有相同字段的多个记录以及具有相同构造函数的多个变体。一般不推荐,但非常好且定义明确。

当您定义两条共享某些字段名称的记录时,字段的最后一个定义会隐藏之前的定义,例如

type t1 = {x : int}
type t2 = {x : char}

let x {x} = x

x 函数的类型为 t2 -> char。这是因为类型推断是语法驱动的,并且从字段的名称到其记录的类型。在 OCaml 的现代版本中,您可以使用 类型定向构造函数消歧 来访问阴影字段,例如

let x : t1 -> _ = fun {x} -> x

由于我们约束了类型,编译器将在指定的记录中查找该字段。

这是一个相当新的功能,在此之前,只有两个字段名称消歧选项。事实上,两者仍然是选项并且可能是推荐的。

第一个选项是将每条记录放在自己的模块中。这是我个人的喜好。事实上,我根本不喜欢在我的接口中有记录或变体,并且总是将它们打包在模块中并将它们作为实现细节隐藏在接口下。

module T1 = struct
  type t = {x : int}
end

module T2 = struct
  type t = {x : char}
end

let x_of_t1 {T1.x} = x
let x_of_t2 {T2.x} = x

但最好在每个模块中提供val x : t -> _功能,并且不依赖于内部表示而使用模块接口。

下一个也是最后一个选项是使用旧的和丑陋的前缀来消除名称歧义。不是真正的解决方案(因为名称现在不同)但它很常见并且并不像看起来那么糟糕,例如,

type t1 = {t1_x : int}
type t2 = {t2_x : char}

我个人不喜欢这个解决方案,但它在各种程序分析项目中很常见,在这些项目中,你有很多中间语言都共享相同的结构,但负载不同。例如,来自解析树的常量 Pconst of string * loc 对比抽象语法树 (AST) 中的常量 Aconst of int * loc 对比类型化 AST 中的常量 Tconst of int * typ * loc,等等在。在这里使用前缀是实用的,cf。 T.Const vs Tconst,一字经济! :),以及根深蒂固的传统。尽管我个人仍然更喜欢模块而不是所有其他选项。