OCaml的模块系统-2
Table of Contents
1 简介
这篇文章是Introduction to Objective Caml第12,13章关于模块系统的笔记, 并不是完整翻译。它比前面一篇更详细地介绍了OCaml的模块系统,包含了更多 细节。
2 OCaml的模块系统
OCaml的模块系统分为三大部分:signatures(签名,接口), structures(结构, 实现),和functors(函子,用于结构的函数).
注意:下面所说的结构都是指模块的结构,不是c语言中的结构体那样的结构。
使用模块系统有好几个原因。最简单的原因是每个结构有它自己的命名空间, 所以使用模块减少命名冲突。另一个原因是在文件中显式指定结构的签名进行 抽象。
2.1 结构和签名
命名结构如下定义:
module ModuleName = struct implementation end
模块名必须大写字母开头。实现可以包含一个.ml文件中出现的任何定义。
OCaml中record类型的label是平坦的,如果一个文件中的两个记录类型使用 同一个标签名定义,第一个定义将丢失。模块使用分离的命名空间解决这个 问题。
module A = struct type t = {name : string; phone : string } end module B = struct module Set = struct let empty = [] let add x l = x :: l let mem = List.mem end let rec unique already_read = print_string "> "; let line = read_line () in if not (Set.mem line already_read) then ( print_endline line; unique (Set.add line already_read) ) else unique already_read ;; (* 测试命名空间时 不让它运行 try unique Set.empty with End_of_file -> () *) type t = { name : string; salary : float } end let jason = { A.name = "Jason"; A.phone = "626-555-1212" } let bob = { B.name = "Bob"; B.salary = 180. }
访问模块中的组件(类型,值,记录标签,嵌套模块等)通过 ModuleName.identifier 的方式。
一个命名签名可以使用module type定义:
module type ModuleName = sig signature end
一般命名签名全部大写。上面的集合可以如下定义:
module type SETSIG = sig type 'a set val empty : 'a set val add : 'a -> 'a set -> 'a set val mem : 'a -> 'a set -> bool end module Set:SETSIG = struct type 'a set = 'a list let empty = [] let add x l = x :: l let mem = List.mem end
2.2 模块定义
2.2.1 模块不是一等公民(first-class)
模块不是表达式,不能作为函数参数,也不能作为函数的返回结果。
从编译器观点来看,一个程序的生命周期有两个阶段:编译时和运行时。模 块表达式可以在编译时计算。因此不会降低性能。
2.2.2 let module表达式
let module ModuleName = module_expression in body_expression
模块表达式 module_expression 定义一个模块(struct … end块),模 块定义在 body_expression 中。 let module 表达式常用来在局部中 重命名模块。
module ModuleWithALongName ... let f x = let module M = ModuleWithALongName in ...
同样地,它可以用来在局部重定义一个现有的模块。在下面的示例中, 重定义了String模块,所以在函数体中 String.identifier 指向本地定 义的模块,并不是标准库(这里主要用来进行调试)
let f x = let module String = struct let create n = eprintf "Allocating a string of length %n%!" n; String.create n ... end in function body
另一个let module表达式的用法是允许类型和异常在局部定义。
let f x = let module M = struct exception Abort end in let g y = ... if done then raise M.Abort in try map g x with M.Abort message -> ...
2.3 递归模块
可以递归定义模块。
module rec Name1 : Signature1 = struct_expression1
and Name2 : Signature2 = struct_expression2
.
.
.
and Namen : Signaturen = struct_expressionn
type 'a ubtree = Node of 'a * 'a ubtree list module rec Tree : sig val map : ('a -> 'b) -> 'a ubtree ->'b ubtree end = struct let map f (Node (x, children)) = Node (f x, Forest.map f children) end and Forest : sig val map : ('a -> 'b) -> 'a ubtree list -> 'b ubtree list end = struct let map f l = List.map (Tree.map f) l end
简单结构定义并不常用,常和functor一起使用。
2.4 include指令
include允许一个结构包含另一个结构或签名的全部内容。 include语句可以重用现有的定义来创建模块和签名。
2.4.1 使用include扩展模块
include用于签名,直接包含现有签名。
module type CHOOSE_SET_SIG = sig include SETSIG val choose : 'a set -> 'a option end (* 系统显示实际签名 *) module type CHOOSE_SET_SIG = sig type 'a set val empty : 'a set val add : 'a -> 'a set -> 'a set val mem : 'a -> 'a set -> bool val choose : 'a set -> 'a option end
2.4.2 使用include扩展实现
include语句用于实现。和签名相同
(* choose 类型不匹配 *) module ChooseSet : CHOOSE_SET_SIG = struct include Set let choose = function | x :: _ -> Some x | [] -> None end Error: Signature mismatch: ... Values do not match: val choose : 'a list -> 'a option is not included in val choose : 'a set -> 'a option
类型不匹配的错误是因为我们包含了一个抽象模块Set,'a set是一个抽象 类型不是一个list。
一个解决方法就是从Set模块复制代码到ChooseSet模块。这当然不好。我们 不能重用现有的实现,我们的代码会变长,等等。如果我们访问原始的未抽 象实现,就会出现另一个问题——我们只能包含未抽象的set实现,它将暴露 出set是使用list实现的。
我们采用一个未抽象的实现SetInternal。Set模块使用签名SETSIG对模块 SetInternal进行抽象; ChooseSet包含SetInternal模块,就可以访问内部 实现了。 Set模块和ChooseSet模块是"朋友",它们各自的实现共享了内部 知识,而它们的公共签名是抽象的。
module SetInternal = struct type 'a set = 'a list let empty = [] let add x l = x :: l let mem = List.mem end module Set : SETSIG = SetInternal module ChooseSet : CHOOSE_SET_SIG = struct include SetInternal let choose = function | x :: _ -> Some x | [] -> None end
2.5 抽象,友元,和模块隐藏
module Sets : sig module Set : SETSIG module ChooseSet : CHOOSE_SET_SIG end = struct module Set = struct type 'a set = 'a list let empty = [] let add x l = x :: l let mem = List.mem end module ChooseSet = struct include Set let choose = function | x :: _ -> Some x | [] -> None end end
在Sets模块中的Set和ChooseSet模块没有约束,所以它们的实现是公开的。 这允许ChooseSet直接引用Set实现(这种情况下,Set和ChooseSet模块就是友 元)。Sets模块的签名对它们进行抽象。
2.5.1 在不兼容的签名上使用include
在上面的示例中,看起来并不需要两个单独的模块ChooseSet和Set。
出乎意料的,在实践中由于程序使用不兼容的签名,这种例子发生的比看起 来的多。例如,我们编写一个程序使用了两个不同的库。每个库都有自己的 Set实现,我们希望使用统一的Set实现。不幸的是,签名不兼容——在第一个 库中,add函数为val add : 'a -> 'a set -> 'a set;但是第二个库定义 的add类型为val add : 'a set -> 'a -> 'a set。 假定希望使用第一个库 的Set签名。 一种解决方法就是修改第二个库中所有调用Set.add函数地方。 当然,这种方法很费事,它也不是我们想要做的。
另一种方法是在第二个库中继承一个Set2模块。这个过程比较简单:
- 包含Set模块
- 重定义add到正确的签名。
Set2模块只是一个包装。
module type SET2_SIG = sig type 'a set val empty : 'a set val add : 'a set -> 'a -> 'a set val mem : 'a -> 'a set -> bool end module Set2 : SET2_SIG = struct include Set let add l x = Set.add x l end
这并不会产生性能问题,大多数情况下OCaml的编译器会inline调用Set2.add函 数(也就是说,在编译时完成参数顺序转换)。
2.6 共享约束
这个示例中还有一个问题没有解决。在组合起来的程序中,第一个库使用原 始的Set模块,第二个库使用Set2。我们希望从一个库到另一个库传递值,包 括集合.不过,就像定义中指示的,'a Set.set和'a Set2.set的类型是不同 的抽象类型,当使用'a Set.set类型的值替换希望使用'a Set2.set类型的值 的地方,或者相反。就会产生类型不匹配的编译错误。
当然,我们也会希望类型不同。但在这里,我们希望定义变成透明的。我们知 道两种类型的集合是相通的——Set2只是Set的包装。我们如何让'a Set.set和'a Set2.set相等?
解决方案叫做共享约束。
signature ::= signature with type typename = type
module Set2 : SET2_SIG with type 'a set = 'a Set.set = struct include Set let add l x = Set.add x l end # let s = Set2.add Set.empty 1;; val s : int Set2.set = <abstr> # Set.mem 1 s;; - : bool = true
约束指定了类型'a Set2.set和'a Set.set是相同的。也可以说,它们共享一 个类型。因为两个类型相等,集合值就可以自由传递给这两个集合实现。
3 函子(Functors)
OCaml中的模块可以使用其它模块作为参数.
为集合提供自定义的相等测试:
module type EQUAL = sig type t val equal : t -> t -> bool end module MakeSet (Equal : EQUAL) = struct open Equal type elt = Equal.t type t = elt list let empty = [] let mem x s = List.exists (equal x) s let add x s = x :: s let find x s = List.find (equal x) s end (* 构造一个示例 *) module StringCaseEqual = struct type t = string let equal s1 s2 = String.lowercase s1 = String.lowercase s2 end module SSet = MakeSet (StringCaseEqual);; # let s = SSet.add "Great Expectations" SSet.empty;; val s : string list = ["Great Expectations"] # SSet.mem "great exPectations" s;; - : bool = true # SSet.find "great exPECTATIONS" s;; - : StringCaseEqual.t = "Great Expectations"
使用functor需要记住以下几点:
- 一个函子的参数必须是一个模块,或其它函子
- 语法上,模块和函子的标识符必须总是大写开头.函子的参数必须使用括号, 并且需要签名.应用函子时也需要括号,比如 MakeSet (StringCaseEqual) .
- 模块和函子都不是一等公民.因此,不能作为数据结构或参数,并且不能
在函数中进行模块定义.
还需要注意一点,新定义的集合实现SSet不再是多态的,它的元素类型由 Equal模块定义.当使用模块作为参数时经常会损失多态性,因为参数的目的 就是为不同类型的元素定义不同的行为.虽然损失多态性是不便的,但在实 践中这很少成为问题,因为模块可以针对每个特定的类型使用函子进行构造.
3.1 共享约束
在MakeSet的例子中,我们忽略了集合类型的签名.这让集合的实现变得可见 (例如,SSet.add返回一个string list).我们可以定义一个签名来隐藏实现, 防止后面的程序依赖实现细节.
module type SET = sig type t type elt val empty : t val mem : elt -> t -> bool val add : elt -> t -> t val find : elt -> t -> elt end module MakeSet (Equal : EQUAL) : SET with type elt = Equal.t = struct open Equal type elt = Equal.t type t = elt list let empty = [] let mem x s = List.exists (equal x) s let add x s = x :: s let find x s = List.find (equal x) s end module SSet = MakeSet (StringCaseEqual);; # let s = SSet.add "Great Expectations" SSet.empty;; val s : SSet.t = <abstr> # SSet.mem "great exPectations" s;; - : bool = true # SSet.find "great exPECTATIONS" s;; - : SSet.elt = "Great Expectations"
共享约束SET with type elt = Equal.t 是重要的. SET签名中的类型elt 是抽象的,如果MakeSet直接使用SET签名返回一个模块,则类型SSet.elt将变 成抽象的,并且不能使用.
module MakeSet (Equal : EQUAL) : SET = struct open Equal type elt = Equal.t type t = elt list let empty = [] let mem x s = List.exists (equal x) s let add x s = x :: s let find x s = List.find (equal x) s end module SSet = MakeSet (StringCaseEqual);; # let s = SSet.add "Great Expectations" SSet.empty;; Characters 17-37: let s = SSet.add "Great Expectations" SSet.empty;; ^^^^^^^^^^^^^^^^^^^^ Error: This expression has type string but an expression was expected of type SSet.elt = MakeSet(StringCaseEqual).elt
这个错误消息显示类型string和SSet.elt不匹配–事实上,只知道类型SSet.elt和 MakeSet(StringCaseEqual).elt是相等的.
3.2 模块共享约束
共享约束还可以约束两个模块相等,包含模块中的所有类型. 当一个模块中的很多类型都需要约束到新模块的一部分时,这非常有用.
module type SET = sig module Equal : EQUAL type t type elt = Equal.t val empty : t val mem : elt -> t -> bool val add : elt -> t -> t val find : elt -> t -> elt end module MakeSet (EqArg : EQUAL) : SET with module Equal = EqArg = struct module Equal = EqArg type elt = Equal.t type t = elt list let empty = [] let mem x s = List.exists (Equal.equal x) s let add x s = x :: s let find x s = List.find (Equal.equal x) s end
3.3 使用函子进行模块重用
借助于Set类实现Map
module type VALUE = sig type value end (* map的签名 *) module type MAP = sig type t (* map类型 *) type key (* 键的类型 *) type value (* 值的类型 *) val empty : t val add : t -> key -> value -> t val find : t -> key -> value end module MakeMap (Equal : EQUAL) (Value : VALUE): MAP with type key = Equal.t (* 签名的类型约束 *) with type value = Value.value = struct type key = Equal.t (* 键的类型由Equal模块给出 *) type value = Value.value (* 值的类型由Value模块给出 *) type item = Key of key | Pair of key * value module EqualItem = struct type t = item (* 函数的参数使用了模式匹配 *) let equal (Key key1 | Pair (key1, _)) (Key key2 | Pair (key2, _)) = Equal.equal key1 key2 end module Set = MakeSet (EqualItem);; (* 使用集合保证了键是唯一的 *) type t = Set.t let empty = Set.empty let add map key value = Set.add (Pair (key, value)) map let find map key = (* item定义中使用Key key就是为了这里不用使用dummy value,因为 value的类型是由Value模块决定的,dummy value没办法写出来 *) match Set.find (Key key) map with | Pair (_, value) -> value | Key _ -> raise (Invalid_argument "find") end
3.4 高阶函子(Higher-order functors)
高阶函子使用其它函子作为一个参数.高级函子在实践中很少用到.
例如,如果我们可以有多种方法构造一个集合(比如列表,树,或其它数据结构),我 们希望可以使用这些集合中的一种构造一个map.解决方法是把MakeSet函子作 为参数传递给MakeMap.
module MakeMap (Equal : EQUAL) (Value : VALUE) (MakeSet : functor (Equal : EQUAL) -> SET with type elt = Equal.t) : MAP with type key = Equal.t with type value = Value.value = struct ...(* 和上面的 MakeMap实现相同,不过这里的MakeSet是由参数传 递进来的,因此可以替换,上面的MakeMap中的MakeSet是固定的. *) end
这些类型太复杂了.高阶函子比较难理解,在实践中谨慎使用.
3.5 递归模块和函子
type 'set element = Int of int | Set of 'set module rec SetEqual : EQUAL with type t = Set.t element = struct type t = Set.t element let equal = ( = ) end and Set : SET with type elt = SetEqual.t = MakeSet (SetEqual)
Set.elt需要SetEqual.t,而SetEqual.t需要Set.t.递归模块可以解决这个问 题.
注意:这里的MakeSet不能使用签名中包含Equal模块的那个版本,否则会出现 如下签名不匹配的错误:
and Set : SET with type elt = SetEqual.t = MakeSet (SetEqual) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Error: In this `with' constraint, the new definition of elt does not match its original definition in the constrained signature: Type declarations do not match: type elt = SetEqual.t is not included in type elt = Equal.t
因为with type或with module是对现有的签名进行修改, 如下示例:
# module type S1 = sig type t module M : sig type u end end;; module type S1 = sig type t module M : sig type u end end # module type S2 = S with type t = int;; module type S2 = sig type t = int module M : sig type u end end # module N = struct type u end;; module N : sig type u end # module type S3 = S with module M = N;; module type S3 = sig type t module M : sig type u = N.u end end
with type或with module返回的是修改后的签名。也就是说SetEqual的签名 是:
sig type t = Set.t element (* 显式类型 *) val equal : t -> t -> bool end
而作为函子MakeSet参数的签名是EQUAL:
sig type t (* 抽象类型 *) val equal : t -> t -> bool end
这里约束Set.elt的类型为SetEqual.t,但是这两个t的类型是不同的,因此会 错误,在OCaml 4.00.1中会有错误。
在实践中,递归模块比非递归模块少的多.
3.6 一个完整示例
实现一个基于红黑树的集合.
type comparison = LT | EQ | GT module type COMPARE = sig type t val compare : t -> t -> comparison end module type SET = sig type t type elt val empty : t val add : elt -> t -> t val mem : elt -> t -> bool val find : elt -> t -> elt val compare : t -> t -> comparison end module MakeSet (CompArg : COMPARE) : SET with type elt = CompArg.t = struct module Comp = CompArg type elt = Comp.t type color = Red | Black type t = Leaf | Node of color * elt * t * t let empty = Leaf let balance = function | Black, z, Node (Red, y, Node (Red, x, a, b), c), d | Black, z, Node (Red, x, a, Node (Red, y, b, c)), d | Black, x, a, Node (Red, z, Node (Red, y, b, c), d) | Black, x, a, Node (Red, y, b, Node (Red, z, c, d)) -> Node (Red, y, Node (Black, x, a, b), Node (Black, z, c, d)) | a, b, c, d -> Node (a, b, c, d) let add x s = let rec insert = function | Leaf -> Node (Red, x, Leaf, Leaf) | Node (color, y, a, b) as s -> match Comp.compare x y with | LT -> balance (color, y, insert a, b) | GT -> balance (color, y, a, insert b) | EQ -> s in match insert s with | Node (_, y, a, b) -> Node (Black, y, a, b) | Leaf -> raise (Invalid_argument "insert") let rec find x = function | Leaf -> raise Not_found | Node (_, y, left, right) -> match Comp.compare x y with | LT -> find x left | GT -> find x right | EQ -> y let mem x s = try ignore (find x s); true with Not_found -> false let rec to_list l = function | Leaf -> l | Node (_, x, left,right) -> to_list (x :: to_list l right) left let rec compare_lists l1 l2 = match l1, l2 with | [], [] -> EQ | [], _ :: _ -> LT | _::_, [] -> GT | x1::t1, x2::t2 -> match Comp.compare x1 x2 with | EQ -> compare_lists t1 t2 | LT | GT as cmp -> cmp let compare s1 s2 = compare_lists (to_list [] s1) (to_list [] s2) end type 'set element = Int of int | Set of 'set module rec Compare : COMPARE with type t = MySet.t element = struct type t = MySet.t element let compare x1 x2 = match x1, x2 with | Int i1, Int i2 -> if i1 < i2 then LT else if i1 > i2 then GT else EQ | Int _, Set _ -> LT | Set _, Int _ -> GT | Set s1, Set s2 -> MySet.compare s1 s2 end and MySet : SET with type elt = Compare.t = MakeSet (Compare) ;; # let s = MySet.add (Int 5) MySet.empty;; val s : MySet.t = <abstr> # let s = MySet.add (Int 6) s;; val s : MySet.t = <abstr> # MySet.find (Int 5) s;; - : MySet.elt = Int 5 # let t = MySet.add (Int 6) s;; val t : MySet.t = <abstr> # MySet.compare t s;; - : comparison = EQ
注意不能像书中的示例那样使用with module,参见这里。 在SET签名中去掉COMPARE就行了。