OCaml的模块系统-1
Table of Contents
1 简介
这篇文章是Using, Understanding,and Unraveling The OCaml Language 中关于模块的学习笔记,不是完整翻译。
使用模块系统的好处有几个:可以把大程序分成好几块分别编译.为大程序添加 结构让它们更容易理解.更精确地说,模块支持,有时是强制组件之间的链接声 明(接口),因此让大程序可维护和可重用.另外,通过强制抽象,模块让程序更 安全.
2 使用模块
2.1 基本模块
基本模块是 structures,一组语句集合.写为 struct p1 … pn end .第一个例子是一个栈的实现:
module Stack = struct type 'a t = {mutable elements : 'a list } let create () = { elements = [] } let push x s = s.elements <- x :: s.elements let pop s = match s.elements with | h :: t -> s.elements <- t; h | [] -> failwith "Empty stack" end;; # let s = Stack.create ();; val s : '_a Stack.t = {Stack.elements = []} # Stack.push 1 s;; - : unit = () # Stack.push 2 s;; - : unit = () # Stack.pop s;; - : int = 2
通过点符号引用模块的组件.
使用 open S 指令可以省略模块名前缀和点.一个模块也可以是另一个模块 的子模块:
module T = struct module R = struct let x = 0 end let y = R.x + 1 end
注意在一个模块 Q 中使用 open T.R 指令让 T.R 的所有组件在模块 Q 后面可见,但并不添加这些组件到模块 Q .
系统像推导值的类型一样推导模块的签名.基本模块的类型叫做签名 signatures,是一系列类型声明.写为 sig s1 … sn end. 例如,系统显示出 Stack 的类型为:
module Stack : sig type 'a t = { mutable elements : 'a list; } val create : unit -> 'a t val push : 'a -> 'a t -> unit val pop : 'a t -> 'a end
不同形式的声明:
| 声明类型 | 形式 |
| values | val x : δ |
| abstract types | type t |
| manifest types | type t = τ |
| exceptions | exception E |
| classes | class z: object … end |
| sub-modules | module X:S |
| module types | module type T [ = M ] |
显式的签名约束可以用来限制系统推导出的类型,和类型约束限制推导出的 表达式类型一样.签名约束写为(M:S),其中 M 是一个模块, S 是一个签名.另外 module X : S = M 是 module X = (M : S) 的语法糖.
明确地说,一个签名约束包含两方面:第一,它检查结构是否符合签名;也就是 说, S 中的所有声明必须在 M 中定义;第二,它让在 M 中但不在 S 中的组件 不可访问. 例如:
module S : sig type t val y : t end = struct type t = int let x = 1 let y = x + 1 end
这时, S.x 和 S.y + 1 将产生错误. 前者是因为x在S中不可见, 后者是因为S.y的 类型是抽象类型S.t,与int类型不兼容.
签名约束经常用来强制类型抽象.比如,前面的Stack模块暴露了它的内部表示.这 允许不通过Stack.create直接创建栈:
Stack.pop { Stack.elements = [2; 3] };;
为了防止混乱,可以对stack的实现进行抽象,强制只能通过Stack.create创建 栈:
module Astack : sig type 'a t val create : unit -> 'a t val push : 'a -> 'a t -> unit val pop : 'a t -> 'a end = Stack;;
抽象也可以用来从一个结构中产生2个同构但不兼容的视图。例如,所有货币 表示为浮点数,所有的货币都不相等,也不能混合。货币是同构但是不相交 的结构,分别表示为不同的单元Euro和Dollar。在OCaml中可以使用签名约束 来实现。
module Float = struct type t = float let unit = 1.0 let plus = ( +. ) let prod = ( *. ) end;; module type CURRENCY = sig type t val unit : t val plus : t -> t -> t val prod : float -> t -> t end;;
注意签名CURRENCY中的乘法操作使用一个外部的浮点数。
module Euro = (Float : CURRENCY);; module Dollar = (Float : CURRENCY);;
在Float中类型t是具体的,所以它可以用于浮点数。与此相反,在模块Euro 和Dollar中它是抽象的,因此Euro.t和Dollar.t不兼容。
let euro x = Euro.prod x Euro.unit;; Euro.plus (euro 10.0) (euro 20.0);; Euro.plus (euro 50.0) Dollar.unit;; (* 类型错误 *)
注意Euro和Dollar之间并没有代码副本(只是签名限制)。
这个模式可以进行轻微的变化来提供一个模块的多个视图。例如,一个模 块可以在一个给定的上下文中使用一个限制的接口,这样就可以限制一些操 作。
module type PLUS = sig type t val plus : t -> t -> t end;; module Plus1 = (Euro : PLUS) module type PLUS_Euro = sig type t = Euro.t val plus : t -> t -> t end;; module Plus2 = (Euro : PLUS_Euro)
在上面,类型Plus1.t与Euro.t不兼容。在下面,类型t是部分抽象的,并且和 Euro.t兼容;视图Plus2允许操作视图Euro中的值。with记法允许为一个签名 添加类型相等。表达式 PLUS with type t = Euro.t 是签名
sig type t = Euro.t val plus : t -> t -> t end
的缩写。
with记法是创建部分抽象类型的便利方式,经常内联使用:
module Plus = (Euro : PLUS with type t = Euro.t);; let t = Plus.plus Euro.unit Euro.unit;; Euro.prod 5. t;; (* 这里Euro.t和Plus.t是相同的类型 *)
2.1.1 分离编译
模块也被用来帮助进行分离编译。通过下面的方法来完成,一个编译单元A由 两个文件组成:
- 实现文件a.ml包含一组语句序列,就像 struct … end 中的语句。
- 接口文件a.mli(可选)是一组声明,和 sig … end 类似。
令一个编译单元B就像访问结构一样访问A,通过点记法A.x或直接open A。我 们假定源文件为:a.ml, a.mli, b.ml。没有B的接口文件。编译过程描述如 下:
| 命令 | 编译 | 创建文件 |
| ocamlc -c a.mli | A的接口 | a.cmi |
| ocamlc -c a.ml | A的实现 | a.cmo |
| ocamlc -c b.ml | B的实现 | b.cmi(生成默认接口文件,推导出的接口) b.cmo |
| ocamlc -o myprog.exe a.cmo b.cmo | 链接 | myprog.exe |
这个程序和下面的代码是相同的:
module A : sig (* a.mli的内容 *) end = struct (* a.ml的内容 *) end module B = struct (* b.ml的内容 *) end
模块定义的顺序和链接命令行上的.cmo对象文件顺序是一致的。
2.2 参数化的模块
一个函子,写为 functor (S:T) -> M ,是一个从模块到模块的函数。
module type T = sig type t val x : t val g : t -> t end ;; module M = functor (X : T) -> struct type u = X.t * X.t let y = X.g (X.x) end module S1 = struct type t = int let x = 1 let g v = v * v end module S2 = struct type t = string let x = "" let g v = v ^ v end module T1 = M (S1);; module T2 = M (S2);;
模块T1和T2可以像常规结构一样使用。注意T1和T2共享了代码(?是指共享了 M的代码?)。
3 模块的高级用法
在这一节,我们使用一个实际的银行系统示例来展示模块的特性。
银行账号管理。因为安全原因,客户和银行必须有不同的账号访问权限。这 可以通过为客户和银行提供不同的账号视图来完成。
module type CLIENT = (* 客户端视图 *) sig type t type currency val deposit : t -> currency -> currency val retrieve : t -> currency -> currency end;; module type BANK = (* 银行视图 *) sig include CLIENT val create : unit -> t end;;
我们从银行的基本模型开始:账户信息提供给客户。当然,只有银行可以创建 帐户,并且防止客户伪造账户,抽象地说,它是给客户的。
(* 这里用了functor的语法糖, *) module Old_Bank (M : CURRENCY): BANK with type currency = M.t = struct type currency = M.t type t = { mutable balance : currency } let zero = M.prod 0.0 M.unit and neg = M.prod (-1.0) let create () = { balance = zero } let deposit c x = if x > zero then c.balance <- M.plus c.balance x; c.balance let retrieve c x = if c.balance > x then deposit c (neg x) else c.balance end module Post = Old_Bank (Euro);; module Client : CLIENT with type currency = Post.currency and type t = Post.t = (* 这两个是类型约束 *) Post;;
这个模型是脆弱的,因为所有信息都在账号内部。 例如,如果客户丢失了他的账号,他将丢失他的钱,因为银行并没有保存任 何记录。而且,安全依赖于类型抽象不被打破。
无论如何,这个示例已经演示了模块化的好处:客户和银行拥有银行账号的不 同视图。因此,银行可以创建一个账号,然后银行或用户就可以存款,但是 用户不能创建新账号。
let my_account = Post.create ();; Post.deposit my_account (euro 100.);; Client.deposit my_account (euro 100.);;
此外,几个账号可以用不同的货币创建,并且不能混合使用,这样的错误将 被类型检查发现:
module Citybank = Old_Bank (Dollar);; let my_dollar_account = Citybank.create ();; Citybank.deposit my_account;; (* 类型错误 *) Citybank.deposit my_dollar_account (euro 100.0);; (* 类型错误 *)
更进一步,银行的实现可以保留它的接口进行更改。我们使用这样的能力去 构建一个更健壮——更真实——的系统。银行的实现保存账号信息到银行的数据 库,而客户端只用给出账户号码。
module Bank (M : CURRENCY) : BANK with type currency = M.t = struct let zero = M.prod 0.0 M.unit and neg = M.prod (-1.0) type t = int type currency = M.t type account = { number : int; mutable balance : currency } (* 银行数据库 *) let all_accounts = Hashtbl.create 10 and last = ref 0 let account n = Hashtbl.find all_accounts n let create () = let n = incr last; !last in Hashtbl.add all_accounts n {number = n; balance = zero}; n let deposit n x = let c = account n in if x > zero then c.balance <- M.plus c.balance x; c.balance (* 注意,这个和Old_Band的语义不同,返回的是实际取出的钱数 *) let retrieve n x = let c = account n in if c.balance > x then (c.balance <- M.plus c.balance (neg x); x) else zero end;;
使用函子可以创建几个银行。和一般的函数应用(函数式编程中,应用就是函数 调用)一样,他们拥有独立私有的数据库。
module Central_Bank = Bank (Euro);; module Banque_de_France = Bank (Euro);;

浙公网安备 33010602011771号