2.4 ファンクタを使った型の抽象化

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
    

ここで起きている問題は、 SETelement 型を抽象型として定義しているため、ファンクタの返した内容の element 型とファンクタの引数に与えられた型 t との等価性に関する情報が失われてしまっていることです。 その結果、 WrongStringSet.elementstring が同じ型でなくなり、 WrongStringSet の操作に string が使えなくなってしまっています。 この例からもわかるように、 SET のシグネチャに現われる elementElt.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.setNoCaseStringSet.set は型が合わず互換性がありません。 これは正しい振舞です。 どちらの集合型も同じ型(文字列)の要素を保持しますが、使用している順序付けが異なり、各操作を通じて維持する必要がある不変条件が異なります(通常の順序に関する狭義増加と、大文字小文字を無視した順序での狭義増加)。 もし、 NoCaseStringSet.set 型の値を AbstractStringSet の操作に適用出来てしまうと、不正な結果を返してしまったり、内部のリストが NoCaseStringSet に求められる不変条件を満たさなくなってしまったりするかもしれません。