我正在阅读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 ++中使用*.c
和A.ml
文件。没有这样的能力,似乎模块类型/签名基本上用于密封/隐藏模块的内部或更明确的类型检查/注释,但不用于单独/独立的编译。
实际上,查看OCaml manual section on modules and separate compilation似乎我对C编译单元的类比是破坏的,因为OCaml手册将OCaml编译单元定义为A.mli
和.h
二重奏,而在C / C ++中,.c
文件被粘贴到任何导入{{1}}文件的编译单元。
答案 0 :(得分:6)
执行此类操作的正确方法是执行以下操作:
在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
编译文件:ocamlc -c ordering.mli
在另一个文件中,请参阅已编译的签名:
open Ordering
let main () =
Printf.printf "%b" (StringOrdering.isLess "a" "b")
let () = main ()
编译文件时,会得到预期的类型错误(即string
与Ordering.StringOrdering.t
不兼容)。如果您要删除类型错误,则应将with type t = string
约束添加到StringOrdering
中ordering.mli
的定义。
所以回答第二个问题:是的,在字节码模式下,编译器只需要知道你所依赖的接口,你可以选择在链接时使用哪个实现。默认情况下,本地代码编译不适用(因为模块间优化),但您可以禁用它。
答案 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.ml
和foo.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
的实现来执行优化(内联)。类型检查器从不查看界面中的内容。