Previous Contents Next
Chapter 2 The module system

この章では Objective Caml のモジュールシステムを紹介します。

2.1 Structures

モジュールの主目的は、関連する定義 (データ型の定義とその型に対する処理) を一緒にパッケージ化して、それらの定義をしっかりした名前付けの枠組みの元で管理することにあります。これによって名前の不足や、うっかり名前を混乱するようなことが避けられます。そのようなパッケージは structure と呼ばれ、任意の定義列を持つ struct...end という形で導入します。structure は通常 module 束縛によって名前を与えられます。例として、優先度付きキューの型とそれに対する処理をまとめた structure を示します。
#module PrioQueue =
   struct
     type priority = int
     type 'a queue = Empty | Node of priority * 'a * 'a queue * 'a queue
     let empty = Empty
     let rec insert queue prio elt =
       match queue with
         Empty -> Node(prio, elt, Empty, Empty)
       | Node(p, e, left, right) ->
           if prio <= p
           then Node(prio, elt, insert right p e, left)
           else Node(p, e, insert right prio elt, left)
     exception Queue_is_empty
     let rec remove_top = function
         Empty -> raise Queue_is_empty
       | Node(prio, elt, left, Empty) -> left
       | Node(prio, elt, Empty, right) -> right
       | Node(prio, elt, (Node(lprio, lelt, _, _) as left),
                         (Node(rprio, relt, _, _) as right)) ->
           if lprio <= rprio
           then Node(lprio, lelt, remove_top left, right)
           else Node(rprio, relt, left, remove_top right)
     let extract = function
         Empty -> raise Queue_is_empty
       | Node(prio, elt, _, _) as queue -> (prio, elt, remove_top queue)
   end;;
module PrioQueue :
  sig
    type priority = int
    and 'a queue = Empty | Node of priority * 'a * 'a queue * 'a queue
    val empty : 'a queue
    val insert : 'a queue -> priority -> 'a -> 'a queue
    exception Queue_is_empty
    val remove_top : 'a queue -> 'a queue
    val extract : 'a queue -> priority * 'a * 'a queue
  end
structure の外で、内容物に参照するときは「ドット表記」を使います。ドット表記とは structure 名で限定した識別子です。例えば、文脈から見て値が来るところで PrioQueue.insert と書けば、structure PrioQueue の中で定義されている関数 insert になります。同様に、型のコンテキストで PrioQueue.queue と書けば、PrioQueue の中で定義されている型 queue になります。
#PrioQueue.insert PrioQueue.empty 1 "hello";;
- : string PrioQueue.queue =
PrioQueue.Node (1, "hello", PrioQueue.Empty, PrioQueue.Empty)
2.2 Signatures

signature は structure のインターフェイスです。signature は structure の内容物や型が外部からアクセス可能かどうかを定めます。structure の内容物を隠したり (つまりローカル関数定義) 、限定された型や内容物を外部からアクセスできるようにしたりするのに使えます。以下の signature は優先度付きキューの 3 つの処理 emptyinsertextract を定めますが、補助関数 remove_top は定めません。同様に queue 型を抽象化します (型の具体的な構造を表記しない) 。
#module type PRIOQUEUE =
   sig
     type priority = int         (* still concrete *)
     type 'a queue               (* now abstract *)
     val empty : 'a queue
     val insert : 'a queue -> int -> 'a -> 'a queue
     val extract : 'a queue -> int * 'a * 'a queue
     exception Queue_is_empty
   end;;
module type PRIOQUEUE =
  sig
    type priority = int
    and 'a queue
    val empty : 'a queue
    val insert : 'a queue -> int -> 'a -> 'a queue
    val extract : 'a queue -> int * 'a * 'a queue
    exception Queue_is_empty
  end
この signature で structure PrioQueue を制限することで、関数 remove_top にはアクセスできず、優先度付きキューの実際の構造表現が隠される、新しい PrioQueue の構図ができます。
#module AbstractPrioQueue = (PrioQueue : PRIOQUEUE);;
module AbstractPrioQueue : PRIOQUEUE
 
#AbstractPrioQueue.remove_top;;
Unbound value AbstractPrioQueue.remove_top
 
#AbstractPrioQueue.insert AbstractPrioQueue.empty 1 "hello";;
- : string AbstractPrioQueue.queue = <abstr>
以下のように、制限は structure の定義のときにもできます。
module PrioQueue = (struct ... end : PRIOQUEUE);;
上記の他に次のような文法もあります。
module PrioQueue : PRIOQUEUE = struct ... end;;
2.3 Functors

functor は structure から structure への「関数」です。パラメータ化した structure を表現するのに使います。structure B が structure A のパラメータとなっているとき、functor F は型にはまったパラメータ B (B の signature であることが) をとり、実際に structure A を返します。functor FB の複数の実装 B1 ...Bn を適用することができ、それぞれ structure A1 ...An が生成されます。

ソートしたリストとして集合を実装する structure で、修業の要素の型とその型の順序づけ関数 (ソートを保つために使う) を提供する structure がパラメータになっているものを例としてあげます。
#type comparison = Less | Equal | Greater;;
type comparison = Less | Equal | Greater
 
#module type ORDERED_TYPE =
   sig
     type t
     val compare: t -> t -> comparison
   end;;
module type ORDERED_TYPE = sig type t val compare : t -> t -> comparison end
 
#module Set =
   functor (Elt: ORDERED_TYPE) ->
     struct
       type element = Elt.t
       type set = element list
       let empty = []
       let rec add x s =
         match s with
           [] -> [x]
         | hd::tl ->
            match Elt.compare x hd with
              Equal   -> s         (* x is already in s *)
            | Less    -> x :: s    (* x is smaller than all elements of s *)
            | Greater -> hd :: add x tl
       let rec member x s =
         match s with
           [] -> false
         | hd::tl ->
             match Elt.compare x hd with
               Equal   -> true     (* x belongs to s *)
             | Less    -> false    (* x is smaller than all elements of s *)
             | Greater -> member x tl
     end;;
module Set :
  functor (Elt : ORDERED_TYPE) ->
    sig
      type element = Elt.t
      and set = element list
      val empty : 'a list
      val add : Elt.t -> Elt.t list -> Elt.t list
      val member : Elt.t -> Elt.t list -> bool
    end
functor Set に ordered type の実装である structure を適用すると、この型の処理の集合を得られます。
#module OrderedString =
   struct
     type t = string
     let compare x y = if x = y then Equal else if x < y then Less else Greater
   end;;
module OrderedString :
  sig type t = string val compare : 'a -> 'a -> comparison end
 
#module StringSet = Set(OrderedString);;
module StringSet :
  sig
    type element = OrderedString.t
    and set = element list
    val empty : 'a list
    val add : OrderedString.t -> OrderedString.t list -> OrderedString.t list
    val member : OrderedString.t -> OrderedString.t list -> bool
  end
 
#StringSet.member "bar" (StringSet.add "foo" StringSet.empty);;
- : bool = false
2.4 Functors and type abstraction

PrioQueue の例で述べたように、set の実際の実装を隠すのはいいことです。この structure の使用者が「sets がリストであること」に頼らないようになり、後で sets のコードを書き換えることなくリストから別のもっといい実装に切り替えることができます。これは Set を適当な functor signature で制限することで為しえます。
#module type SETFUNCTOR =
   functor (Elt: ORDERED_TYPE) ->
     sig
       type element = Elt.t      (* concrete *)
       type set                  (* abstract *)
       val empty : set
       val add : element -> set -> set
       val member : element -> set -> bool
     end;;
module type SETFUNCTOR =
  functor (Elt : ORDERED_TYPE) ->
    sig
      type element = Elt.t
      and set
      val empty : set
      val add : element -> set -> set
      val member : element -> set -> bool
    end
 
#module AbstractSet = (Set : SETFUNCTOR);;
module AbstractSet : SETFUNCTOR
 
#module AbstractStringSet = AbstractSet(OrderedString);;
module AbstractStringSet :
  sig
    type element = OrderedString.t
    and set = AbstractSet(OrderedString).set
    val empty : set
    val add : element -> set -> set
    val member : element -> set -> bool
  end
 
#AbstractStringSet.add "gee" AbstractStringSet.empty;;
- : AbstractStringSet.set = <abstr>
上記のような型の制約をもっとエレガントに書こうと思う人は、functor が返す structure の signature に名前を付けたいと考えるかも知れません。その場合は制約の中に signature を用いてください。
#module type SET =
   sig
     type element
     type set
     val empty : set
     val add : element -> set -> set
     val member : element -> set -> bool
   end;;
module type SET =
  sig
    type element
    and set
    val empty : set
    val add : element -> set -> set
    val member : element -> set -> bool
  end
 
#module WrongSet = (Set : functor(Elt: ORDERED_TYPE) -> SET);;
module WrongSet : functor (Elt : ORDERED_TYPE) -> SET
 
#module WrongStringSet = WrongSet(OrderedString);;
module WrongStringSet :
  sig
    type element = WrongSet(OrderedString).element
    and set = WrongSet(OrderedString).set
    val empty : set
    val add : element -> set -> set
    val member : element -> set -> bool
  end
 
#WrongStringSet.add "gee" WrongStringSet.empty;;
This expression has type string but is here used with type
  WrongStringSet.element = WrongSet(OrderedString).element
ここで SET が型 element を抽象化しているため、functor の結果の element と引数の t が型等価であることを忘れてしまっていて問題が発生します。結果的に、WrongStringSet.elementstring とは型が異なるため、WrongStringSet の処理に文字列を適用することができません。上で見せたように、signature SET 中の型 elementElt.t と同じ型であることを宣言することが重要です。残念ながら、これは上記の方法では不可能です。というのは SET が定義されている文脈では Elt が存在しないからです。この問題を解決するため、Objective Caml では with type というコンストラクトを提供します。これを使うと signature に型等価性を追加できます。
#module AbstractSet = 
   (Set : functor(Elt: ORDERED_TYPE) -> (SET with type element = Elt.t));;
module AbstractSet :
  functor (Elt : ORDERED_TYPE) ->
    sig
      type element = Elt.t
      and set
      val empty : set
      val add : element -> set -> set
      val member : element -> set -> bool
    end
この場合のように単純な structure なら、functor を定義してその結果を制限する別の文法があります。
module AbstractSet(Elt: ORDERED_TYPE) : (SET with type element = Elt.t) =
  struct ... end;;
functor の結果の型を抽象化するのは、今説明したように、高度な型安全性を提供する効果的なテクニックです。文字列の順序付けで、標準とは異なる structure OrderedString の実装を考えてみましょう。例えば、大文字小文字を区別しない文字列比較は以下のようになります。
#module NoCaseString =
   struct
     type t = string
     let compare s1 s2 =
       OrderedString.compare (String.lowercase s1) (String.lowercase s2)
   end;;
module NoCaseString :
  sig type t = string val compare : string -> string -> comparison end
 
#module NoCaseStringSet = AbstractSet(NoCaseString);;
module NoCaseStringSet :
  sig
    type element = NoCaseString.t
    and set = AbstractSet(NoCaseString).set
    val empty : set
    val add : element -> set -> set
    val member : element -> set -> bool
  end
 
#NoCaseStringSet.add "FOO" AbstractStringSet.empty;;
This expression has type
  AbstractStringSet.set = AbstractSet(OrderedString).set
but is here used with type
  NoCaseStringSet.set = AbstractSet(NoCaseString).set
AbstractStringSet.setNoCaseStringSet.set は互換性のない型で、それらの型の値もマッチしないことに注意してください。これは正しい動作です。たとえこれらの型が要素に同じ型 (文字列型) を持っていたとしても、これらは型が異なる順序づけから生成されたものなので、処理 (標準の順序付けと大文字小文字を区別しない順序づけ) によって異なる不変性が保持される必要があります。AbstractStringSet の処理に NoCaseStringSet.set の型の値を適用することは正しくない結果を生みかねなく、NoCaseStringSet の不変性を破壊するリストが生成できてしまいます。

2.5 Modules and separate compilation

これまでのモジュールの例は対話式システムのものでした。しかしモジュールは大規模なバッチコンパイルプログラムにこそ便利なモノです。そのようなプログラムは、コンパイルユニットと呼ばれるいくつかのファイルに分割する必要があります。コンパイルユニットは別々にコンパイルすることができ、変更を加えても最小限の再コンパイルで済みます。

Objective Caml では、コンパイルユニットは特殊な structure と signature になり、ユニット同士の関係はモジュールシステムという点で簡単に説明できます。コンパイルユニット a は 2 つのファイルから成ります。 トップレベル環境で次の様に打ち込んだ様に、 どちらのファイルも A という名前の structure (二つのファイルの basename a と同じで、最初の文字を大文字にしたもの) を定義することになります。
module A: sig (* contents of file a.mli *) end
        = struct (* contents of file a.ml *) end;;
コンパイルユニットを定義するファイルは、ocamlc -c コマンドを使って 別々にコンパイルすることが出来ます (-c オプションは、「コンパイルのみ 行い、リンクを行わない」という意味です) 。これにより、 インタフェースファイル (拡張子 .cmi) とコンパイルされたオブジェクトファイル (拡張子 .cmo) が生成されます。全てのファイルがコンパイルされていれば、 .cmo ファイルは ocaml コマンドを使ってリンクすることが出来ます。 例えば、次の子万とは二つのコンパイルユニット (aux, main) からなるプログラムをコンパイルし、リンクします。
$ ocamlc -c aux.mli                     # produces aux.cmi
$ ocamlc -c aux.ml                      # produces aux.cmo
$ ocamlc -c main.mli                    # produces main.cmi
$ ocamlc -c main.ml                     # produces main.cmo
$ ocamlc -o theprogram aux.cmo main.cmo
プログラムは、トップレベルで次のフレーズを打ち込んだ様に振舞います。
module Aux: sig (* contents of aux.mli *) end
          = struct (* contents of aux.ml *) end;;
module Main: sig (* contents of main.mli *) end
           = struct (* contents of main.ml *) end;;
実際、Main は、Aux を参照できます。main.ml 及び main.mli に含まれている定義と宣言は、aux.mli で 定義がエクスポートされていれば aux.ml の定義を 、Aux.ident により参照できます。

.cmo ファイルが ocaml に、リンクフェース中にどの順番で 渡されるかは、モジュール定義がされた順番で決定されます。すなわち、 上記の例では、Aux が最初に現れ、Main
Aux を参照できますが、AuxMain を参照できません。

トップレベルの structure のみ分割コンパイルファイルにマッピングされ、 ファンクタやモジュールタイプはマッピングされないことに注意して下さい。 しかし、すべてのモジュールクラスオブジェクトは、structure のコンポーネント として現れることが出来ます。従って、structure の中にファンクタやモジュール型 を置くことで、ファイルにマップさせることが出来ます。


Previous Contents Next