【问题标题】:OCaml module types and separate compilationOCaml 模块类型和单独编译
【发布时间】:2012-03-23 17:02:28
【问题描述】:

我正在阅读OCaml lead designer's 1994 paper on modules, types, and separate compilation.Norman Ramseyanother question 中向我指出)。我知道这篇论文讨论了 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 抱怨语法错误。我理解签名的目的是允许某人根据模块签名编写代码,而无需访问实现(模块结构)。

我承认我不确定我在this rather old paper on separate compilation 中遇到的语法:module A : B,但这让我想知道是否存在这样或类似的语法(不涉及函子)以允许某人仅基于模块类型,链接时提供的实际模块结构,类似于在 C/C++ 中使用*.h*.c 文件的方式。如果没有这种能力,似乎模块类型/签名基本上是用于密封/隐藏模块内部或更明确的类型检查/注释,而不是用于单独/独立编译。

实际上,看着OCaml manual section on modules and separate compilation,我与C 编译单元的类比似乎被打破了,因为OCaml 手册将OCaml 编译单元定义为A.mlA.mli 二重奏,而在C/C++ 中@ 987654342@ 文件被粘贴到任何导入的.c 文件的编译单元。

【问题讨论】:

  • 正如托马斯的回答暂时所说,默认情况下本机编译没有单独的编译。我希望有,为此我在 mantis 中输入了一个功能愿望:caml.inria.fr/mantis/view.php?id=4389。如果有人知道如何在 OCaml 中获得单独的本地编译(正如 Thomas 暂时声称的那样),我会非常有兴趣了解它。
  • @PascalCuoq:为什么说不能在Ocaml中单独编译原生代码?当然可以。
  • 实际上,我只是按照 Thomas 的回答中的建议修改了这两个文件,实际上您可以分别编译字节码 (ocamlc) 或本机 (ocamlopt)。
  • @AndreasRossberg 这里没有重复我的功能愿望的余地,但是看看ocamldep的输出,如果你不移动它们,它对应于.cmx文件之间的实际依赖关系防止编译器将内容从一个内联到另一个。请注意,愿望是在“不会修复”中解决的,而不是在“不需要更改”中解决的。另见frama-c.com/u3cat/download/CuoqICFP09.pdf 中的“单独编译”备注。这些从未受到挑战。我很高兴在 OCaml 中有单独的原生编译,但很多人认为没有。

标签: module ocaml


【解决方案1】:

这样做的正确方法是执行以下操作:

  1. 在 ordering.mli 中写:

    (* 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. 编译文件:ocamlc -c ordering.mli

  3. 在另一个文件中,参考编译后的签名:

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

    编译文件时,您会收到预期的类型错误(即stringOrdering.StringOrdering.t 不兼容)。如果要消除类型错误,应在ordering.mliStringOrdering 的定义中添加with type t = string 约束。

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

【讨论】:

  • 嘿,你的答案回来了!您能否扩展答案的“您可以禁用它”部分?也许我在 mantis 中输入的功能希望阐明了为什么这对我很重要:caml.inria.fr/mantis/view.php?id=4389
  • 您所指的错误是关于ocamldepocamlopt 如果在路径中没有找到对应的.cmx 文件,则禁用跨模块优化;如果您将没有交叉编译的编译单元链接在一起应该可以正常工作;如果你把事情混在一起,我真的不知道会发生什么:-)
  • 谢谢,这两个文件现在分开编译。此外,至少对于这个简单的例子,我也能够使用 ocamlopt 来分别编译这两个文件,即使我理解你的回答是只有 ocamlc 才有可能。
  • @Thomas 我的错误报告是ocamldep 错误报告和ocamlopt 错误报告的未折叠波形。不过,我会尝试移动文件来欺骗编译器。
  • @Thomas 令我恼火的是,您必须实际命名具有签名的模块。此外,在您的回复的第 3 步中,不是“引用编译的签名”而是“虚拟”模块(StringOrdering)。这个 StringOrdering 模块实际上是在链接时提供的实际实现的占位符。因此,当提供“真正的”StringOrdering.cmo 时,步骤 3 中的“open”语句必须从 open Ordering 更改为其他内容。因此,当您必须更改实际链接的来源时,我看不到真正的单独编译。或者也许我什么都没有。
【解决方案2】:

您可能只是对显式模块和签名定义之间的关系以及通过 .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的模块,而不是一个模块type。 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
    

    如果你想隐藏类型t的定义,你需要创建一个不同的模块,其中定义是抽象的。将旧模块变成新模块的操作称为密封,通过: 运算符表示。

    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 &lt;foo.h&gt;,因为它缺少任何形式的命名空间;在 OCaml 中,Foo.bar 自动引用在编译单元 foo 中定义的 bar,如果范围内没有其他名为 Foo 的模块。

    ¹ 实际上,本机代码编译器查看Foo 的实现以执行优化(内联)。类型检查器只查看界面中的内容。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-03-01
      • 1970-01-01
      • 2016-02-02
      • 2012-02-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多