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新手),但我到目前为止不知所措.
显微镜中的模块比它看起来更微妙(如果你的眼睛在某一点上釉,请跳到第二部分.)
让我们看看如果将所有内容放在同一个文件中会发生什么.这应该是可能的,因为单独的计算单元不会增加类型系统的功率. (注意:对于此文件以及文件a.*和b.*的任何测试使用单独的目录,否则编译器将看到可能令人困惑的编译单元A和B.)
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的接口,然后定义B和A的接口.
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时,这指的是模块A的名为t的类型字段.
在Ocaml中,这种微妙的发生是相当罕见的.但是你试着在语言的一角捅,这就是潜伏在那里的东西.
那么当有两个编译单元时会发生什么?更接近的模型是将A视为一个以模块B为参数的仿函数. B所需的签名是接口文件b.mli中描述的签名.类似地,B是一个函数,它采用模块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.(当然,除非是递归模块,但在这里我们试图了解为什么我们可以’没有他们就过去了.)
定义生成类型是副作用
根本的关键点是类型t = {x:int}是一个生成型定义.如果此片段在程序中出现两次,则定义两种不同的类型. (Ocaml采取步骤并禁止您在同一模块中定义两个具有相同名称的类型,但在顶层除外.)
实际上,正如我们在上面看到的,在模块实现中键入t = {x:int}是生成类型定义.它的意思是“定义一个名为d的新类型,它是带有字段的记录类型……”.相同的语法可以出现在模块接口中,但它有不同的含义:那里,它意味着“模块定义了类型t,它是一种记录类型……”.
由于定义生成类型两次会产生两种不同的类型,因此A定义的特定生成类型无法通过模块A(其签名)的规范完全描述.因此,使用这种生成类型的程序的任何部分实际上都是使用A的实现,而不仅仅是它的规范.
当你了解它时,定义一个生成类型,它是一种副作用.这种副作用发生在编译时或程序初始化时(这两者之间的区别仅在你开始查看仿函数时出现,我不会在这里做.)因此,重要的是要记录这种副作用何时发生:它在定义(编译或加载)模块A时发生.
因此,为了更具体地表达这一点:模块A中的类型定义类型t = {x:int}被编译为“let t be type#1729,一个新类型,它是一个带有字段的记录类型……”. (新类型意味着与以前定义的任何类型不同的类型.). B的定义定义了一个类型#1729.
由于模块B依赖于模块A,因此必须在B之前加载A.但是A的实现明显使用B的实现.这两者是相互递归的. Ocaml的错误信息有点令人困惑,但你确实超越了语言的界限.