我有一个分解,其中模块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新手),但我我到目前为止都处于亏损状态。
答案 0 :(得分:5)
(如果你的眼睛在某一点上釉,请跳到第二部分。)
让我们看看如果将所有内容放在同一个文件中会发生什么。这应该是可能的,因为单独的计算单元不会增加类型系统的功率。 (注意:使用单独的目录以及文件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
时,这会引用模块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并完成它是首先要考虑的事情。导致这种结构的原始问题是什么?