OCaml模块类型和分离编译

8

我正在阅读OCaml首席设计师1994年关于模块、类型和独立编译的论文(由Norman Ramsey另一个问题中向我指出)。我了解到该论文讨论了OCaml当前模块类型/签名系统的起源。其中,作者提出了类型声明在签名中的不透明解释(以允许分别编译),以及具有表现力的显式类型声明。为了演示OCaml模块签名符号试图解决的问题类型,我尝试编写了以下代码:

在文件ordering.ml(或.mli - 我都尝试过)中(文件A):

module type ORDERING = sig
 type t
 val isLess : t -> t -> bool
end

并且在文件useOrdering.ml中(B文件):

open Ordering
module StringOrdering : ORDERING
  let main () =
    Printf.printf "%b" StringOrdering.isLess "a" "b"
  main ()

这里的想法是期望编译器在编译第二个文件时抱怨模块StringOrdering上没有足够的类型信息来对StringOrdering.isLess应用进行类型检查(从而激发对with type语法的需求)。然而,尽管文件A按预期编译,但文件B导致3.11.2的ocamlc抱怨语法错误。我理解签名意味着允许某人基于模块签名编写代码,而无需访问实现(模块结构)。
我承认我不确定语法:module A : B,我在这篇相当古老的有关分离编译的论文中遇到过它,但这让我想知道是否存在这样或类似的语法(不涉及functor),以允许某人仅基于模块类型编写代码,在链接时提供实际的模块结构,就像在C/C++中可以使用*.h*.c文件一样。如果没有这样的能力,那么模块类型/签名基本上是为了封装/隐藏模块的内部或更明确的类型检查/注释,而不是用于分离/独立编译。

实际上,查看OCaml模块和分离编译手册后,我发现我的类比与C编译单元不符,因为OCaml手册将OCaml编译单元定义为A.mlA.mli二合一,而在C/C++中,.h文件则被粘贴到任何导入.c文件的编译单元中。


正如Thomas的回答所说,原生编译默认情况下没有独立编译。我希望有这样的功能,并在mantis上提交了一个特性请求:http://caml.inria.fr/mantis/view.php?id=4389。如果有人知道如何在OCaml中实现独立的原生编译(正如Thomas刚才声称的可能),我会非常感兴趣听听相关信息。 - Pascal Cuoq
1
@PascalCuoq:为什么你说在Ocaml中无法单独编译本机代码?当然可以。 - Andreas Rossberg
实际上,我只是按照Thomas的回答建议修改了这两个文件,确实可以分别编译字节码(ocamlc)或本地代码(ocamlopt)。 - Marcus Junius Brutus
@AndreasRossberg这里没有重复我的功能愿望的空间,但是请看ocamldep的输出,它对应于.cmx文件之间的实际依赖关系,如果您不将它们移动以防止编译器将一个文件中的内容内联到另一个文件中。请注意,该愿望已在“不修复”中解决,而不是“无需更改”。请参见http://frama-c.com/u3cat/download/CuoqICFP09.pdf中的“单独编译”备注。这些从未受到挑战。我很高兴听到OCaml有单独的本地编译,但许多人认为没有。 - Pascal Cuoq
3个回答

6
正确的做法是按照以下步骤进行:
  1. In ordering.mli write:

    (* This define the signature *)
    module type ORDERING = sig
      type t
      val isLess : t -> t -> bool
    end
    
    (* This define a module having ORDERING as signature *)
     module StringOrdering : ORDERING
    
  2. Compile the file: ocamlc -c ordering.mli

  3. In another file, refer to the compiled signature:

    open Ordering
    
    let main () =
      Printf.printf "%b" (StringOrdering.isLess "a" "b")
    
    let () = main ()
    

    When you compile the file, you get the expected type error (ie. string is not compatible with Ordering.StringOrdering.t). If you want to remove the type error, you should add the with type t = string constraint to the definition of StringOrdering in ordering.mli.

回答你的第二个问题:是的,在字节码模式下,编译器只需要知道你所依赖的接口,你可以在链接时选择使用哪个实现。默认情况下,本地代码编译不支持此功能(因为存在跨模块优化),但您可以禁用它。


嘿,你的回答回来了!你能详细解释一下你回答中“你可以禁用它”的部分吗?也许我在Mantis中提交的功能愿望可以说明为什么这对我很重要:http://caml.inria.fr/mantis/view.php?id=4389 - Pascal Cuoq
你所提到的错误与 ocamldep 有关。如果 ocamlopt 在路径中找不到相应的 .cmx 文件,它将禁用跨模块优化;如果你链接在一起的编译单元没有进行交叉编译,那么应该可以正常工作;如果混合使用,我真的不知道会发生什么 :-) - Thomas
谢谢,现在这两个文件可以分别编译了。此外,至少对于这个简单的例子,我也能够使用ocamlopt来分别编译这两个文件,尽管我理解你的答案是只有ocamlc才能做到这一点。 - Marcus Junius Brutus
@Thomas 我的错误报告是一个未折叠的波形,其中包含了一个 ocamldep 错误报告和一个 ocamlopt 错误报告。我会尝试移动文件来欺骗编译器。 - Pascal Cuoq
@Thomas 让我感到恼火的是您必须实际命名拥有签名的模块。此外,在您回答的第3步中,人们不是“参考编译后的签名”,而是引用一个“虚拟”的模块(StringOrdering)。这个StringOrdering模块实际上是为在链接时提供真正实现的占位符。因此,当提供“真正的” StringOrdering.cmo 时,步骤3中的“open”语句将必须从 open Ordering 更改为其他内容。所以当你不得不为实际链接更改源代码时,我并没有看到真正的分离编译。或者可能是我没理解到什么。 - Marcus Junius Brutus
显示剩余3条评论

4

您可能对显式模块和签名定义与通过.ml/.mli文件隐式定义模块之间的关系感到困惑。

基本上,如果您有一个名为a.ml的文件,并在其他文件中使用它,则就好像您已经编写了以下内容:

module A =
struct
  (* content of file a.ml *)
end

如果您也有a.mli文件,则就好像您已经编写了它。
module A :
sig
  (* content of file a.mli *)
end =
struct
  (* content of file a.ml *)
end

请注意,这仅定义了一个名为A的模块,而不是模块类型。通过此机制无法为A的签名赋予名称。
使用A的另一个文件可以仅编译a.mli而不提供a.ml。然而,你需要确保在需要透明地使用所有类型信息时进行公开。例如,假设你要定义一个整数映射:
(* intMap.mli *)
type key = int
type 'a map
val empty : 'a map
val add : key -> 'a -> 'a map -> 'a map
val lookup : key -> 'a map -> 'a option
...

在这里,key被透明化了,因为任何客户端代码(属于描述此签名的模块IntMap)需要知道它才能将某个内容添加到映射中。然而,map类型本身可以(且应该)保持抽象,因为客户端不应该干涉其实现细节。
与C头文件的关系在于它们基本上只允许透明类型。在Ocaml中,你有选择的余地。

3

module StringOrdering : ORDERING 是一个模块声明。您可以在签名中使用它,以表示该签名包含一个名为 StringOrdering 的模块字段,并具有签名 ORDERING。这在模块中没有意义。

您需要在某个地方定义一个模块,实现您需要的操作。模块定义可以如下:

module StringOrderingImplementation = struct
  type t = string
  let isLess x y = x <= y
end

如果您想隐藏类型的定义,您需要创建一个不同的模块,其中定义是抽象的。将旧模块制作成新模块的操作称为封装,并通过:运算符表示。
module StringOrderingAbstract = (StringOrdering : ORDERING)

那么 StringOrderingImplementation.isLess "a" "b" 是类型安全的,而 StringOrderingAbstract.isLess "a" "b" 无法进行类型检查,因为 StringOrderingAbstract.t 是一个抽象类型,它与 string 或任何其他预定义类型不兼容。事实上,无法构建类型为 StringOrderingAbstract.t 的值,因为该模块不包含任何构造函数。

当你有一个编译单元 foo.ml,它是一个模块 Foo,这个模块的签名由接口文件 foo.mli 给出。也就是说,文件 foo.mlfoo.mli 相当于模块定义。

module Foo = (struct (*…contents of foo.ml…*) end :
              sig (*…contents of foo.mli…*) end)

当编译一个使用 Foo 的模块时,编译器只会查看 foo.mli(或者更确切地说是它的编译结果: foo.cmi),而不是 foo.ml¹。这就是接口和分离编译如何结合在一起的。C语言需要 #include <foo.h>,因为它缺乏任何形式的命名空间;在OCaml中,如果没有其他叫做 Foo 的模块在作用域内,Foo.bar 自动引用编译单元 foo 中定义的 bar

¹ 实际上,本地代码编译器会查看 Foo 的实现以执行优化(内联)。类型检查器从不查看除接口之外的任何内容。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接