nTest

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
OCaml的模块系统-2

OCaml的模块系统-2

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模块。这个过程比较简单:

  1. 包含Set模块
  2. 重定义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需要记住以下几点:

  1. 一个函子的参数必须是一个模块,或其它函子
  2. 语法上,模块和函子的标识符必须总是大写开头.函子的参数必须使用括号, 并且需要签名.应用函子时也需要括号,比如 MakeSet (StringCaseEqual) .
  3. 模块和函子都不是一等公民.因此,不能作为数据结构或参数,并且不能 在函数中进行模块定义.

    还需要注意一点,新定义的集合实现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就行了。

Date: 2013-02-02 星期六

Author: nTest

Org version 7.8.11 with Emacs version 24

Validate XHTML 1.0
posted on 2013-02-28 14:55  nTest  阅读(841)  评论(0编辑  收藏  举报