この章では Objective Caml のモジュールシステムを紹介します。
モジュールの主目的は、関連する定義 (データ型の定義とその型に対する処理) を一緒にパッケージ化して、それらの定義をしっかりした名前付けの枠組みの元で管理することにあります。これによって名前の不足や、うっかり名前を混乱するようなことが避けられます。そのようなパッケージは 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)
signature は structure のインターフェイスです。signature は structure の内容物や型が外部からアクセス可能かどうかを定めます。structure の内容物を隠したり (つまりローカル関数定義) 、限定された型や内容物を外部からアクセスできるようにしたりするのに使えます。以下の signature は優先度付きキューの 3 つの処理 empty 、insert 、extract を定めますが、補助関数 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;;
functor は structure から structure への「関数」です。パラメータ化した structure を表現するのに使います。structure B が structure A のパラメータとなっているとき、functor F は型にはまったパラメータ B (B の signature であることが) をとり、実際に structure A を返します。functor F は B の複数の実装 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.element は string とは型が異なるため、WrongStringSet の処理に文字列を適用することができません。上で見せたように、signature SET 中の型 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
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.set と NoCaseStringSet.set は互換性のない型で、それらの型の値もマッチしないことに注意してください。これは正しい動作です。たとえこれらの型が要素に同じ型 (文字列型) を持っていたとしても、これらは型が異なる順序づけから生成されたものなので、処理 (標準の順序付けと大文字小文字を区別しない順序づけ) によって異なる不変性が保持される必要があります。AbstractStringSet の処理に NoCaseStringSet.set の型の値を適用することは正しくない結果を生みかねなく、NoCaseStringSet の不変性を破壊するリストが生成できてしまいます。
2.5 |
Modules and separate compilation |
|
これまでのモジュールの例は対話式システムのものでした。しかしモジュールは大規模なバッチコンパイルプログラムにこそ便利なモノです。そのようなプログラムは、コンパイルユニットと呼ばれるいくつかのファイルに分割する必要があります。コンパイルユニットは別々にコンパイルすることができ、変更を加えても最小限の再コンパイルで済みます。
Objective Caml では、コンパイルユニットは特殊な structure と signature になり、ユニット同士の関係はモジュールシステムという点で簡単に説明できます。コンパイルユニット a は 2 つのファイルから成ります。
-
実装ファイル a.ml。定義の列を持ち、struct...end コンストラクトの中身に似ています。
- インターフェイスファイル a.mli。仕様の列を持ち、sig...end コンストラクト中身に似ています。
トップレベル環境で次の様に打ち込んだ様に、
どちらのファイルも 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 を参照できますが、Aux は Main を参照できません。
トップレベルの structure のみ分割コンパイルファイルにマッピングされ、
ファンクタやモジュールタイプはマッピングされないことに注意して下さい。
しかし、すべてのモジュールクラスオブジェクトは、structure のコンポーネント
として現れることが出来ます。従って、structure の中にファンクタやモジュール型
を置くことで、ファイルにマップさせることが出来ます。