Chapter 2 モジュールシステムこのページは最後に更新されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。 last mod. 2008-08-15 (金) 13:27:16
この章では Objective Caml のモジュールシステムを紹介します。 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 type '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) Signaturessignature は structure のインターフェイスです。 signature は structure の内容物が外部からアクセス可能か、そしてどのような型としてアクセス可能かを定めます。 structure の内容物を隠したり (つまりローカル関数定義) 、 型に制限をした上で内容物を外部からアクセスできるようにしたりするのに使えます。 以下に示す signature は優先度付きキューの 3 つの処理 empty 、insert 、extract を定めますが、補助関数 remove_top は定めません。 同様に以下では、具体的な構造を具体的な型(concrete type)として提供しないことによって、 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 type '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> こうしたsignatureによる制限は、structure の定義と同時に行うことも出来ます。 module PrioQueue = (struct ... end : PRIOQUEUE);; 上記と同様のことを行う別の構文として、以下のような構文も用意されています。 module PrioQueue : PRIOQUEUE = struct ... end;; Functorsfunctor は structure から structure への「関数」であり、パラメータ化された structure を表現するために使用されます。 例えば、structure B によりパラメータ化された structure A は仮引数 B(structure Bのsignatureを持つことが期待される) を持ち、実際のstructure Aを返す functor F として表現されます。 そのようなfunctor Fがあるとき、F に対していくつかのBの実装B_1 ...B_nを適用することで、対応するstructure A_1 ... A_n が得られます。 具体的な例として、ソート済リストとして実装した集合を考えます。 ここで考える集合は集合要素の型と、集合要素の型の上の順序付け関数でパラメータ化されています。 #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 type 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を実装した型に適用することにより、その型に対する集合演算を得ることが出来ます。 #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 type 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 Functors and type abstractionPrioQueue?の例で見たように、集合型の実装を隠蔽することは良い作法です。 集合型の利用者が集合型の内部実装がリストによることに依存しないようにすることで、将来集合型の内部表現をより効率的な構造に変更したとしても、既存のコードを修正せず再利用できます。 実装の隠蔽は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 type 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 type 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 に名前を与えておき、その名前を型の制約中で利用することを試みます。 #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 type 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 type 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型とaddの引数の型との型の等価性に関する情報が失われてしまっています。 その結果、WrongStringSet?.elementがstringとは同じ型でないため、WrongStringSet?の操作にstringが使えなくなってしまっています。 この例からもわかるように、SETのsignatureに現われるelementとElt.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 type set val empty : set val add : element -> set -> set val member : element -> set -> bool end 通常のstructの場合と同様に、functorの結果型を制限しつつfunctorの定義を同時に行う代替構文も用意されています。 module AbstractSet(Elt: ORDERED_TYPE) : (SET with type element = Elt.t) = struct ... end;; functorの結果が内包する型を抽象化することは、これから示すような高度な型安全性を実現する強力な手段となります。 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 type 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?.set と NoCaseStringSet?.set は型が合わず互換性がありません。 これは期待した振舞です。 どちらの集合型も要素としては同じ型(string)を保持しますが、使用している順序付けが異なり、各操作を通じて維持する必要がある不変条件が異なります。 もし、NoCaseStringSet?.set型の値をAbstractStringSet?の操作に適用出来てしまうと、不正な結果を返却してしまったり、内部のリストがNoCaseStringSet?に求められる不変条件を満たさなくなってしまったりするかもしれません。 Modules and separate compilationここまでのモジュールの例は全て対話式システム上での例でした。 しかし、モジュールはより大規模なバッチコンパイルされるようなときもっとも有効です。 そのような類のプログラムでは、ソースをコンパイル単位と呼ばれる個別にコンパイルできるいくつかのファイルに分割し、変化があった場合に再コンパイルが必要となる範囲を最小化します。 Objective Camlでは、コンパイル単位とは、structureやsignatureの特殊なケースで、モジュールシステムの言葉で簡単に説明できます。 コンパイル単位 Aは二つのファイルからなります
両ファイルによりstructure Aが、トップレベルで次のように入力したように定義されます。 module A: sig (* ファイル A.mli の内容 *) end = struct (* ファイル A.ml の内容*) end;; これらコンパイル単位を定義するファイルは個別にocaml -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 (* Aux.mli の内容*) end = struct (* Aux.ml の内容 *) end;; module Main: sig (* Main.mli の内容 *) end = struct (* Main.ml の内容 *) end;; 具体的には、MainはAuxを参照することが出来ます。 Main.ml 及び Main.mli に含まれる定義や宣言はAux.識別子 記法により Aux.mliでエクスポートされた定義にアクセスすることが出来ます。 リンク時にocamlコマンドに与えられる.cmoファイルの順番は、その順番に各モジュールが定義されたものとして解釈されます。 それため、この例ではAuxが最初に現われるためMainはそれを参照できますが、AuxはMainを参照できません。 ここで注意が必要なのは、トップレベルのstructureのみが分割コンパイルされるファイルに分けることが出来、functorやモジュール型(module type)はそのままでは分割コンパイルされるファイルに分けることが出来ません。 幸い、全てのmodule類のオブジェクト(module-class object)はstructureの中におくことが出来るため、structureの中に置いた後、ファイルに分けることが出来ます。 |