nTest

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

OCaml的模块系统-1

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

不同形式的声明:

声明类型形式
valuesval x : δ
abstract typestype t
manifest typestype t = τ
exceptionsexception E
classesclass z: object … end
sub-modulesmodule X:S
module typesmodule type T [ = M ]

显式的签名约束可以用来限制系统推导出的类型,和类型约束限制推导出的 表达式类型一样.签名约束写为(M:S),其中 M 是一个模块, S 是一个签名.另外 module X : S = Mmodule 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.mliA的接口a.cmi
ocamlc -c a.mlA的实现a.cmo
ocamlc -c b.mlB的实现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);;

Date: 2013-02-01 星期五

Author: nTest

Org version 7.8.11 with Emacs version 24

Validate XHTML 1.0
posted on 2013-02-28 14:54  nTest  阅读(836)  评论(0)    收藏  举报