モジュールを「拡張」する― 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 UnixUnix のモジュール型なんだけど、その中のデータ型は元の Unix 中の型との関係が失われている。だから、「拡張」Unix とオリジナルの Unix の間でデータが上手くやりとりでない。

さて、これを何とかして欲しいのですが、考えられる方法は二つ:

  • S with type t = τ を、S.t が具体的な定義を持つ型の場合でも許す
  • module type of M の意味を変更して、内部の全ての型に自動的に type alias を付ける

私には、どちらもあまり問題が無いように思います。ただ、前者は内部の型について全て手で一つ一つ alias を回復しなければいけないのが面倒です。後者の方が簡単かつ、便利だと思うんですが、どうでしょうね。