【问题标题】:How can an OCaml module export a field defined in a dependent module?OCaml 模块如何导出依赖模块中定义的字段?
【发布时间】:2011-12-05 07:35:40
【问题描述】:

我有一个分解,其中模块 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 新手),但我至今不知所措。

【问题讨论】:

    标签: module ocaml


    【解决方案1】:

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

    (如果您的眼睛在某个时候发呆,请跳到第二部分。)

    让我们看看如果将所有内容放在同一个文件中会发生什么。这应该是可能的,因为单独的计算单元不会增加类型系统的能力。 (注意:对于文件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,其签名在 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 被定义(编译或加载)时发生。

    因此,更具体地表达这一点:模块A中的类型定义type t = {x : int}被编译为“让t成为类型#1729,一种新类型,它是一个带有字段的记录类型……”。 (fresh 类型意味着与以前定义的任何类型都不同。)。 B 的定义将 a 定义为具有 #1729 类型。

    由于模块B依赖于模块A,所以A必须在B之前加载。但是A的实现显然使用了B的实现。两者是相互递归的。 Ocaml 的错误信息有点混乱,但你确实超出了语言的界限。

    【讨论】:

    • 这彻底回答了这个问题。但从表面上看,它与我在其他地方看到的几个关于依赖类型的引用不一致,定义在接口中,不会触发通常的循环模块依赖。需要明确的是,具体问题是 OCaml 中的 记录类型 是生成的,所以这对于非记录类型定义(例如type a = int)来说不是问题吗?或者类似地,如果记录类型是在第三个模块中定义的?
    • @jrk 对,记录类型、普通变体类型和抽象类型是生成的。其他类型的类型定义仅仅是别名。在第三个模块中定义类型是通常的解决方案。
    【解决方案2】:

    (以及A--接口定义或模块定义--this指的是哪一个)。

    A 指的是整个模块 A。在正常的构建过程中,它指的是受 a.mli 中签名约束的 a.ml 中的实现。但是,如果您正在玩弄移动 cmi 之类的技巧-您只能靠自己了 :)

    据我所知,这应该是洁净的。

    我个人将此问题定性为循环依赖,并强烈反对以这种方式构建代码。恕我直言,与解决实际问题相比,它会导致更多的问题和令人头疼的问题。例如。将共享类型定义移动到 type.ml 并完成它是首先想到的。导致这种结构化的最初问题是什么?

    【讨论】:

      猜你喜欢
      • 2019-02-01
      • 1970-01-01
      • 1970-01-01
      • 2019-02-02
      • 2020-10-27
      • 1970-01-01
      • 1970-01-01
      • 2019-01-12
      • 2019-02-11
      相关资源
      最近更新 更多