PrioQueue の例で見たように、集合型の実装を隠蔽することは良い作法です。
集合型の利用者が集合型の内部実装がリストであることに依存しないようにすることで、将来集合型の内部表現をより効率的な構造に変更したとしても、既存のコードを修正せず再利用できます。
実装の隠蔽は Set を適切なファンクタシグネチャで制限することで実現できます。
#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>
上記の型の制約をより綺麗に記述するために、ファンクタの返すストラクチャのシグネチャに名前を与えておき、その名前を型の制約中で利用したいと考える人もいるでしょう。
#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 型を抽象型として定義しているため、ファンクタの返した内容の element 型とファンクタの引数に与えられた型 t との等価性に関する情報が失われてしまっていることです。
その結果、 WrongStringSet.element と string が同じ型でなくなり、 WrongStringSet の操作に string が使えなくなってしまっています。
この例からもわかるように、 SET のシグネチャに現われる element と Elt.t が等価であることを宣言することが必要ですが、 SET が定義される文脈において Elt が存在しないためこれは実現できません。
この問題を克服するため Objective Camlでは with type 構文によりシグネチャに型の等価性情報を追加出来ます。
#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
単純なストラクチャの場合と同様に、結果型を制限しつつファンクタの定義を同時に行う代替構文も用意されています。
module AbstractSet(Elt: ORDERED_TYPE) : (SET with type element = Elt.t) =
struct ... end;;
ファンクタの結果が内包する型を抽象化することは、これから示すような高度な型安全性を実現する強力な手段となります。
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 は型が合わず互換性がありません。
これは正しい振舞です。
どちらの集合型も同じ型(文字列)の要素を保持しますが、使用している順序付けが異なり、各操作を通じて維持する必要がある不変条件が異なります(通常の順序に関する狭義増加と、大文字小文字を無視した順序での狭義増加)。
もし、 NoCaseStringSet.set 型の値を AbstractStringSet の操作に適用出来てしまうと、不正な結果を返してしまったり、内部のリストが NoCaseStringSet に求められる不変条件を満たさなくなってしまったりするかもしれません。