OCaml模块类型和单独编译

时间:2012-03-23 17:02:29

标签: module ocaml

我正在阅读OCaml lead designer's 1994 paper on modules, types, and separate compilation.Norman Ramsey中的another 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应用程序的需求ocamlc语法)。 但是,尽管文件A按预期编译,但文件B导致3.11.2 module A : B抱怨语法错误。我知道签名的目的是允许某人根据模块签名编写代码,而无需访问实现(模块结构)。

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

实际上,查看OCaml manual section on modules and separate compilation似乎我对C编译单元的类比是破坏的,因为OCaml手册将OCaml编译单元定义为A.mli.h二重奏,而在C / C ++中,.c文件被粘贴到任何导入{{1}}文件的编译单元。

3 个答案:

答案 0 :(得分:6)

执行此类操作的正确方法是执行以下操作:

  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不兼容)。如果您要删除类型错误,则应将with type t = string约束添加到StringOrderingordering.mli的定义。

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

答案 1 :(得分: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中,您可以选择。

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

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