第一級モジュール
OCamlには、3.12から「第一級モジュール」の機能が加わっているらしいです。
第一級モジュールとは、第一級オブジェクトとしてモジュールが扱える、つまり変数や関数と同じように関数への受け渡しや代入操作が出来るものです。
公式マニュアル7.14 第一級のモジュールに詳しい説明があります。
簡単な例として、以下のintとfloatをラップするモジュールIntとFloatを使って示していきます。
module type Type = sig type t val compare : t -> t -> int end module Int = struct type t = int let compare (x:int) = compare x end module Float = struct type t = float let compare (x:float) = compare x end |
モジュールは、たとえばIntと書いても第一級オブジェクトとして扱うことは出来ません。
モジュールを第一級オブジェクトとして扱うには、以下の文法を用います。
# let int = (module Int:Type with type t = int);; val int : (module Type with type t = int) = # let float = (module Float:Type with type t = float);; val float : (module Type with type t = float) = |
(module Module : Signature)で第一級オブジェクトを作るわけですね。
ちなみに、シグネチャの省略は出来ません。怒られます。
また、with typeを使わなくても第一級オブジェクトを作ること自体は出来ますが、関数に渡すときに困ります。
# let int = (module Int);; Error: Syntax error |
で、このモジュールを受け取って、2数が等しいか判定するような関数equalは以下のようになります。
let equal (type t') mdl x y = let module T = (val mdl:Type with type t = t') in T.compare x y = |
ここでtypeは、実際に引数として取るわけではなく、引数に伴って決まる型を表すようです。
公式マニュアルの7.13 型変数の命名にも説明があります。
こちらの構文も3.12からなんですね。
また、(val module : Signature)は先ほどと逆で、第一級モジュールにしたモジュールをモジュールに戻すための構文みたいです。
equalの型、及びモジュールを適用した時の型は以下のようになります。
# equal;; - : (module Type with type t = 'a) -> 'a -> 'a -> bool = # equal int;; - : int -> int -> bool = # equal float;; - : float -> float -> bool = |
ここで、equalの第一引数が(module Type with type t = ‘a)になることから、先ほどのwith typeを使っておかないといけないわけですね。
ちなみに、この引数のシグネチャは完全一致する必要があります。
例えば、以下のようなAddableシグネチャとこれを実装するよう拡張したIntを定義します。
module type Addable = sig type t val compare : t -> t -> int val add : t -> t -> t end module Int = struct type t = int let compare (x:int) = compare x let add = (+) end |
AddableはTypeを実装していますが、Addableで型付けしたintをequalに渡すと怒られます。
# let int = (module Int:Addable with type t = int);; val int : (module Addable with type t = int) = # equal int;; Error: This expression has type (module Addable with type t = int) but an expression was expected of type (module Type with type t = 'a) |
ファンクタみたいに、シグネチャが含まれていればOK、ではないわけですね。
ただし、Intを型付けする段階でTypeとして定義することは可能で、これを渡せばちゃんと機能します。
# let int = (module Int:Type with type t = int);; val int : (module Type with type t = int) = # equal int;; - : int -> int -> bool = |
さて、公式マニュアルには、ファンクタと組み合わせて第一級モジュールを使う例が示されています。
let sort (type s) set l = let module Set = (val set : Set.S with type elt = s) in Set.elements (List.fold_right Set.add l Set.empty) let make_set (type s) cmp = let module S = Set.Make(struct type t = s let compare = cmp end) in (module S : Set.S with type elt = s) |
例えば、以下のように使えます。
# make_set compare;; - : (module Set.S with type elt = '_a) = # sort (make_set compare) [1;3;2;4];; - : int list = [1; 2; 3; 4] # sort (make_set compare) [1.2;4.3;2.7; 6.8];; - : float list = [1.2; 2.7; 4.3; 6.8] |
make_setは説明の都合で分割しているだけらしく、実際には以下のようにcmpとlを受け取ってSetを完全に隠蔽したほうがスマートですね。
ま、この場合だとtype構文だけで、第一級モジュールは使ってないわけですが。
let sort (type s) cmp l = let module Set = Set.Make(struct type t = s let compare = cmp end) in Set.elements (List.fold_right Set.add l Set.empty) |
さて、先ほどの例を見ると、Set.elementsする前は、lに含まれる要素を含んだSet.tになっています。
これが直接返せればset_of_listを抽象化できそう……ですが、残念ながらこの場合はローカル定義であるlet moduleのスコープから抜け切れないためエラーになります。
# let to_set (type s) set l = let module Set = (val set : Set.S with type elt = s) in List.fold_right Set.add l Set.empty;;;; Error: This `let module' expression has type Set.t In this type, the locally bound module name Set escapes its scope |
Setは局所定義されているモジュールなので、外部にSet.tを持ち越すことはできないわけですね。
これを無理にやろうとすると、モジュールに参照を埋め込んでモジュール自身の第一級モジュールを返す、という少し複雑なことになります。(もっとスマートな方法があるかもしれないけれど)
(* 参照を持ったSet型のシグネチャ *) module type RefSetType = sig include Set.S val set : t ref end (* 参照を持ったSet型のファンクタ *) module RefSet(S:Type) = struct include Set.Make(S) let set = ref empty end (* compareとlistからRefSetを返す *) let to_set (type s) cmp l = let module Set = RefSet (struct type t = s let compare = cmp end) in let () = (Set.set := List.fold_right Set.add l Set.empty) in (module Set : RefSetType with type elt = s) (* RefSetからlistを返す *) let to_list (type s) set = let module Set = (val set : RefSetType with type elt = s) in Set.fold (fun x l -> x::l) !Set.set [] |
なんかカオスなことになってきました。
使い方としては以下のような感じになります。
# let is = to_set compare [1;4;2;3;5];; val is : (module RefSetType with type elt = int) = # to_list is;; - : int list = [5; 4; 3; 2; 1] # let fs = to_set compare [1.2; 4.3; 2.7; 5.8];; val fs : (module RefSetType with type elt = float) = # to_list fs;; - : float list = [5.8; 4.3; 2.7; 1.2] |
ここまで来ると、to_listをSetの名前空間に押し込めるか、to_listをEnumableみたいなシグネチャで取って任意のモジュールに適用するかしたくなってきます。
が、前者だとオブジェクト指向な感じになってクラス使えばいいじゃないか、という話ですし、後者は第一級モジュールにラップする段階でシグネチャを選ばないといけない関係上、毎回対応するシグネチャにキャストしなければならず面倒なことなります。
多態モジュールみたいなシステムがあれば便利ですが、完全型推論とかの関係で無理なんでしょうかね。
そもそも、第一級モジュールの有効な使い方は、きっとこういう話じゃないんでしょうね……
公式マニュアルや、前回に引き続きosiireさんのサイトに有用な使い方が紹介されてますので、まじめに使いたい方はその辺を参考にされるといいと思います。
間違っても、こんなふうにエセオブジェクト指向をやるための機能ではないと思います、ええ。