我有一个分解,其中模块A定义了一个结构类型,并导出了这个类型的字段,该字段被定义为模块中的值B:
a.ml:
type t = {
x : int
}
let b = B.a
Run Code Online (Sandbox Code Playgroud)
b.ml:
open A (* to avoid fully qualifying fields of a *)
let a : t = {
x = 1;
}
Run Code Online (Sandbox Code Playgroud)
避免循环依赖,因为B只依赖于类型声明(而不是值)A.
a.mli:
type t = {
x : int
}
val b : t
Run Code Online (Sandbox Code Playgroud)
据我所知,这应该是犹太教.但编译器出错了:
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
Run Code Online (Sandbox Code Playgroud)
当然,这一点都特别迟钝,因为不清楚哪个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)
Run Code Online (Sandbox Code Playgroud)
哦,好吧,这不行.很明显,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)
Run Code Online (Sandbox Code Playgroud)
好吧,不.
File "d.ml", line 7, characters 12-18:
Error: Unbound type constructor Asig.t
Run Code Online (Sandbox Code Playgroud)
你看,Asig是签名.签名是模块的规范,不再是; Ocaml中没有签名的微积分.您不能引用签名字段.您只能引用模块的字段.在编写时A.t,这指t的是模块命名的类型字段A.
在Ocaml中,这种微妙的发生是相当罕见的.但是你试着在语言的一角捅,这就是潜伏在那里的东西.
那么当有两个编译单元时会发生什么?更接近的模型是A将模块B视为参数的仿函数.所需的签名B是接口文件中描述的签名b.mli.类似地,B是一个将A其签名a.mli作为参数给出的模块的函数.哦,等等,它有点涉及:A出现在签名中B,所以界面B真正定义了一个带有a A并产生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)
Run Code Online (Sandbox Code Playgroud)
在这里,在定义时A,我们遇到了一个问题:我们还没有A,作为参数传递B.(当然,除非是递归模块,但在这里我们试图了解为什么没有它们我们就无法实现.)
根本的关键点是type t = {x : int}生成型定义.如果此片段在程序中出现两次,则定义两种不同的类型.(Ocaml采取步骤并禁止您在同一模块中定义两个具有相同名称的类型,但在顶层除外.)
实际上,正如我们在上面看到的,type t = {x : int} 在模块实现中是生成类型定义.它的意思是"定义一个名为的新类型,d它是带有字段的记录类型......".相同的语法可以出现在模块接口中,但它有不同的含义:那里,它意味着"模块定义了一种类型t,它是一种记录类型......".
由于定义生成类型两次会创建两种不同的类型,A因此无法通过模块的规范A(其签名)完全描述由其定义的特定生成类型.因此,使用这种生成类型的程序的任何部分实际上都是使用它的实现,A而不仅仅是它的规范.
当你了解它时,定义一个生成类型,它是一种副作用.这种副作用发生在编译时或程序初始化时(这两者之间的区别仅在你开始查看仿函数时出现,我不会在这里做.)因此,重要的是要记录这种副作用何时发生:它在A定义(编译或加载)模块时发生.
因此,为了更具体地表达这一点:type t = {x : int}模块中的类型定义A被编译为"let tbe type#1729,一种带有字段的记录类型的新类型......".(新类型意味着与以前定义的任何类型不同的类型.).定义的B定义a为#1729类型.
由于模块B依赖于模块A,因此A必须先加载B.但执行A明确使用执行B.这两者是相互递归的.Ocaml的错误信息有点令人困惑,但你确实超越了语言的界限.