モジュールを「拡張」する― 3.12.0 の機能を使って…(みるけどうまくいかない)
OCaml のモジュールが大好き!! 3.12.0 インストールしてるよ!という人だけ、読んで下さい。それ以外の人は、あんまりわかんないと思う。
前回のまとめ
綺麗にドキュメント(.mli)を書きつつ、既存OCamlモジュールを「拡張」する方法を紹介しました
(* xmystdlib.ml *) module List = struct let iteri f list = let rec iteri f n = function | [] -> () | x::xs -> f n x; iteri f (n+1) xs in iteri f 0 list end
(* xmystdlib.mli *) module List : sig val iteri : (int -> 'a -> unit) -> 'a list -> unit (** iteration with index *) end
(* mystdlib.ml *) module X = Xmystdlib module List = struct include List (* the original *) include X.list (* the extension *) end
この例では、オリジナルの List のシグナチャを書き下すのが面倒なので、拡張部分だけのシグナチャを書いて、その後、オリジナルと拡張を .mli の無いモジュールで混ぜ混ぜするのでした。
OCaml 3.12 の module type of を使って、もっと簡単に、なりそう…なんだけど…
さて、OCaml 3.12 では、module type of という、モジュールのシグナチャを取り出す便利な機能が加わりました。これを使うと、上の二段構えのテクニックが要らなくなりそうです。なぜなら、 List モジュールのシグナチャは module type of List でオッケーだから:
(* mystdlib.ml *) module List = struct include List (* include the original *) let iteri f list = let rec iteri f n = function | [] -> () | x::xs -> f n x; iteri f (n+1) xs in iteri f 0 list end
(* mystdlib.mli *) module List : sig include module type of List (* include the sig of the original *) val iteri : (int -> 'a -> unit) -> 'a list -> unit (** List version of iteri: it is an iteration but with the position of the element in the list *) end
ご覧のように、3.11.2 では List モジュールのシグナチャをコピペしなきゃいけなくって逃げてたところを、3.12 では逃げたり、コピペする代わりに include module type of List と書けばよい。これだと二段構えの構造がいらなくなるので楽!
本当はここで記事が終わるとカッコいいんですが…
module type of M は場合によってすごい使いにくいョ!!
拡張する元のモジュールで型が定義されていない場合、上の List みたいな場合、include module type of M で問題ないんです。でも、残念ながら、元のモジュールに型があると、こう簡単にはいかない。Unix に usleep を足してみましょう:
(* mystdlib.ml *) module Unix = struct include Unix let usleep sec = ... (* まあ、 C で書くなり、itimer で書くなりやってちょ *) end
(* mystdlib.mli *) module Unix : sig include module type of Unix val usleep : float -> unit (** Stop execution for the given number of seconds *) end
はい、出来た!出来ません…。何故か?これだと、オリジナルの Unix の型と拡張した Mystdlib.Unix の同名の型が別の型と認識されてしまって、Mystdlib.Unix が上手く使えないのです… Mystdlib.Unix だけで閉じていれば問題ないのですが、他のサードパーティーライブラリなどが、オリジナルの Unix の型を使っていると、こいつと Mystdlib.Unix の連携ができません。例えば、サードパーティライブラリが Unix.error を返すので、それを Mystdlib.Unix で処理したい場合です:
let _ = Xunix.Unix.E2BIG = Unix.E2BIG
ここで、E2BIG というエラーの値を比べているのですけど、方や、型は Xunix.Unix.error。もう一方は Unix.error。同じようでいて、OCaml は違うと思っているので、型エラーになってしまう(泣 なんでやーっ。
ほいじゃあ、with type を使って Xunix.Unix.error と、 Unix.error が同じだということを教えてあげればいいじゃないか?
(* mystdlib.mli *) module Unix : sig include module type of Unix with type error = Unix.error val usleep : float -> unit (** Stop execution for the given number of seconds *) end
うん、これで、このモジュール内の error は Unix.error と同じだね!と行きたいところですが、これはエラー!!
File "mystdlib.mli", line 2, characters 10-59: Error: In this `with' constraint, the new definition of error does not match its original definition in the constrained signature: Type declarations do not match: type error = Unix.error is not included in type error = E2BIG | EACCES ... | EOVERFLOW | EUNKNOWNERR of int Their kinds differ.
知らねーよ!Kind 同じだよ!!何とかしてよ!!! ちなみに、いろいろ頑張りましたが、module type of Unix を使った場合、どうやっても無理っぽい。こう言う時は諦めて、元の Xmystdlib と Mystdlib の二段構えの方法を使うしかありません。
そもそも module type of M ってなんだっけか。
そう、まず、module type of M が何か判ってないと、なぜ上が上手く動かないか判らない。
module type of M とは、環境中のモージュール M の型(シグナチャ)です。どういうことか。
module M = struct type t = Foo | Bar let f = function | Foo -> 1 | Bar -> 2 end
とすると、module type of M は、次のモジュール型、S と同じです。
module type S = sig type t = Foo | Bar val f : t -> int end
それが証拠に、
module type S = sig type t = Foo | Bar val f : t -> int end module M : S = struct type t = Foo | Bar let f = function | Foo -> 1 | Bar -> 2 end module type S' = module type of M
を ocamlc -c -i すると、
module type S = sig type t = Foo | Bar val f : t -> int end module M : S module type S' = S
と帰ってきます。S ' = module type of M = S が成り立ってますね。
ところで、ここで、 M をコピーして M' というモジュールを作ると、その型はどうなるでしょうか。S でしょうか?:
...(同上)... module M' = M
これを ocamlc -c -i してやると…
module type S = sig type t = Foo | Bar val f : t -> int end module M : S module type S' = S module M' : sig type t = M.t = Foo | Bar val f : t -> int end
良く見てください。これは S じゃない。type t = M.t = Foo | Bar となっています。OCaml のモジュールに慣れて無い人は何だこりゃと思うかもしれませんが、これも正しい型の定義です。「型 t は M.t と同じで、ちなみに Foo と Bar というコンストラクタが使えますよ」という意味です。この = M.t という "type alias" のおかげで、M.Foo と M'.Foo が同じ型を持ち、比較が可能になるのです。
じゃあ、この type alias, = M.t が無いと、どうなるのか?
...(同上)... module M'' : S = M
こうすると、M'' の型は S、つまり、sig type t = Foo | Bar val f : t -> int end という M.t との関係が無い型になります。OCaml は M''.t は M.t を別の型だと認識しますので、M.Foo と M''.Foo は比較できません。そして、今現在のところ、この元のモジュールと関係の無くなったモジュール型から、関係を回復してあげる方法が OCaml には無いのです。次の式三つは、type alias を回復しようとしているのですが、いずれも kind が合わないと言われます。S with type t = ほげほげ、は、S.t が抽象型の時しか使えないのですね:
module type S'' = S with type t = M.t module type S'' = module type of M with type t = M.t module type S'' = module type of M with type t := M.t
これと同じ事が、module type of Unix を使ったモジュール「拡張」の失敗で起こっています。module type of Unix は Unix のモジュール型なんだけど、その中のデータ型は元の Unix 中の型との関係が失われている。だから、「拡張」Unix とオリジナルの Unix の間でデータが上手くやりとりでない。
さて、これを何とかして欲しいのですが、考えられる方法は二つ:
- S with type t = τ を、S.t が具体的な定義を持つ型の場合でも許す
- module type of M の意味を変更して、内部の全ての型に自動的に type alias を付ける
私には、どちらもあまり問題が無いように思います。ただ、前者は内部の型について全て手で一つ一つ alias を回復しなければいけないのが面倒です。後者の方が簡単かつ、便利だと思うんですが、どうでしょうね。