OCaml模块如何导出依赖模块中定义的字段?

时间:2011-12-05 07:35:41

标签: module ocaml

我有一个分解,其中模块A定义了一个结构类型,并导出了这个类型的字段,该字段被定义为模块B中的值:

a.ml

type t = {
  x : int
}

let b = B.a

b.ml

open A (* to avoid fully qualifying fields of a *)
let a : t = {
  x = 1;
}

避免循环依赖,因为B仅取决于A中的类型声明(而不是值)。

a.mli

type t = {
  x : int
}

val b : t

据我所知,这应该是犹太人。但是编译器出错了:

File "a.ml", line 1, characters 0-1:
Error: The implementation a.ml does not match the interface a.cmi:
       Values do not match: val b : A.t is not included in val b : t

当然,这一点特别迟钝,因为不清楚哪个val b被解释为具有t类型且哪个类型为A.t(以及A - 接口定义或模块定义 - 这是指。)

我假设有一些神秘的规则(沿着“结构字段必须由模块未打开时完全模块限定的名称引用”的语义,在某些时候咬每个OCaml新手),但我我到目前为止都处于亏损状态。

2 个答案:

答案 0 :(得分:5)

显微镜中的模块比看起来更微妙

(如果你的眼睛在某一点上釉,请跳到第二部分。)

让我们看看如果将所有内容放在同一个文件中会发生什么。这应该是可能的,因为单独的计算单元不会增加类型系统的功率。 (注意:使用单独的目录以及文件a.*b.*的任何测试,否则编译器将看到编译单元AB令人困惑。)

module A = (struct
    type t = { x : int }
    let b = B.a
  end : sig
    type t = { x : int }
    val b : t
  end)
module B = (struct
    let a : A.t = { A.x = 1 }
  end : sig
    val a : A.t
  end)

哦,好吧,这不行。很明显,此处未定义B。我们需要更加准确地了解依赖关系链:首先定义A的接口,然后定义B的接口,然后定义BA的实现。

module type Asig = sig
    type t = { x : int }
    type u = int
    val b : t
  end
module B = (struct
    let a : Asig.t = { Asig.x = 1 }
  end : sig
    val a : Asig.t
  end)
module A = (struct
    type t = { x : int }
    let b = B.a
  end : Asig)

嗯,不。

File "d.ml", line 7, characters 12-18:
Error: Unbound type constructor Asig.t

您看,Asig是签名。签名是模块的规范,不再是; Ocaml中没有签名的微积分。您不能引用签名字段。您只能引用模块的字段。当您编写A.t时,这会引用模块t的名为A的类型字段。

在Ocaml中,这种微妙的发生是相当罕见的。但是你试着在语言的一角捅,这就是潜伏在那里的东西。

那么当有两个编译单元时会发生什么?更接近的模型是将A视为一个以模块B为参数的仿函数。 B所需的签名是接口文件b.mli中描述的签名。同样,B是一个函数,它将A中签名的模块a.mli作为参数。哦,等等,它涉及更多:A出现在B的签名中,因此B的界面实际上定义了一个带A并生成的仿函数一个B,可以这么说。

module type Asig = sig
    type t = { x : int }
    type u = int
    val b : t
  end
module type Bsig = functor(A : Asig) -> sig
    val a : A.t
  end
module B = (functor(A : Asig) -> (struct
    let a : A.t = { A.x = 1 }
  end) : Bsig)
module A = functor(B : Bsig) -> (struct
    type t = { x : int }
    let b = B.a
  end : Asig)

在这里,在定义A时,我们遇到了一个问题:我们还没有A,作为参数传递给B。 (当然,除非是递归模块,但在这里我们试图了解为什么没有它们我们就无法实现。)

定义生成型是副作用

基本的关键点是type t = {x : int}是一种生成型定义。如果此片段在程序中出现两次,则定义两种不同的类型。 (Ocaml采取步骤并禁止您在同一模块中定义两个具有相同名称的类型,但在顶层除外。)

事实上,正如我们上面所见,模块实现中的type t = {x : int} 是一种生成类型定义。它的意思是“定义一个名为d的新类型,它是一个带有字段的记录类型......”。相同的语法可以出现在模块接口中,但它有不同的含义:那里,它意味着“模块定义了一个类型t,它是一种记录类型......”。

由于定义生成类型两次会创建两种不同的类型,A定义的特定生成类型无法通过模块A(其签名)的规范完整描述。因此使用此生成类型的程序的任何部分实际上都使用A的实现,而不仅仅是其规范

当你开始研究它时,定义一种生成类型,这是一种副作用。这种副作用发生在编译时或程序初始化时(这两者之间的区别仅在你开始查看仿函数时出现,我不会在这里做。)因此,重要的是要记录这种副作用何时发生:它在定义(编译或加载)模块A时发生。

因此,为了更具体地表达这一点:模块type t = {x : int}中的类型定义A被编译为“let t类型#1729,一种新的类型,它是一种记录类型有一个领域......“。 ( fresh 类型表示与以前定义的任何类型不同的类型。)。 B的定义将a定义为类型#1729。

由于模块B取决于模块A,因此必须在A之前加载B。但A的实施明确使用B的实施。这两者是相互递归的。 Ocaml的错误信息有点令人困惑,但你确实超越了语言的界限。

答案 1 :(得分:2)

  

(以及A - 接口定义或模块定义 - 这是指)。

A指的是整个模块A.使用正常的构建过程,它将引用a.ml中由签名限制的实现。但是,如果你正在玩cmi的技巧,你可以自己动手:)

  

据我所知,这应该是犹太人。

我个人认为这个问题是循环依赖的,并且会强烈反对以这种方式构造代码。恕我直言,它解决了比实际问题更多的问题和令人头疼的问题。例如。将共享类型定义移动到type.ml并完成它是首先要考虑的事情。导致这种结构的原始问题是什么?