OCamlで存在型を試してみた (その2ーーファンクタ)
の続き。
前回は第1級モジュールを非常に綺麗に存在型に対応させられることが示せた。
しかし第1級モジュールを使わずとも、OCamlだとモジュールのシグニチャ自体が存在型的だ。(ただし微妙だが重要な差異もある)
今回はファンクタを通してシグニチャを使うことと存在型の関連について。
ファンクタ
「同じインタフェースを提供するモジュールに対してパラメトリックなコードを書く」という必要がある場合、第1級モジュールを使う手もあるが、OCamlでのより一般的な言語機構は「モジュールを引数にモジュールを返す『関数的なもの』」であるファンクタである。
前回に続いてADDABLE_INT
というシグニチャとそれに合致するnative intとペアノ数を使った実装の二つのモジュールを用意する:
module type ADDABLE_INT = sig type t val zero : t val create : int -> t val to_int : t -> int val add : t -> t -> t end module IntInt = struct type t = int let zero = 0 let create x = x let to_int x = x let add = (+) end module PeanoInt = struct type t = | Zero | Succ of t let zero = Zero let rec create n = if n = 0 then Zero else Succ (create (n-1)) let rec to_int = function | Zero -> 0 | Succ x -> 1 + (to_int x) let rec add x = function | Zero -> x | Succ y -> add (Succ x) y end
前回の記事にはなかった要素として、ゼロに対応する値zero
をシグニチャ・ストラクチャ両方に追加した。
これでt * int
の掛け算を計算する関数をパラメトリックに定義できる:
module MakeMul (M : ADDABLE_INT) = struct let rec mul x n = if n = 0 then M.zero else if n = 1 then x else M.add x (mul x (n-1)) end module MulInt = MakeMul(IntInt) module MulPeano = MakeMul(PeanoInt)
utop # IntInt.to_int @@ MulInt.mul (IntInt.create 5) 6;; - : int = 30 utop # PeanoInt.to_int @@ MulPeano.mul (PeanoInt.create 5) 6;; - : int = 30
MakeMul
ファンクタはADDABLE_INT
に合致するモジュールを受け取り、mul : t -> int -> t
関数を持つモジュールを返す。 ファンクタの内部では渡されたモジュールがどんな実装になっているかはわからないので、インタフェースを通してのみ引数モジュールを使える。
というわけでファンクタの引数のシグニチャに抽象型(この例ではtype t
)があれば、概ね存在型だと見做せる。
ただしTaPLで定義されている存在型で拡張されたシステムFの機能とは違うところもある:
- ファンクタの中でも依然としてモジュールのままで値としてデータ構造に入れたりなどはできない
- 引数となるモジュールごとに新しいモジュールが返されるので、ファンクタの外ではモジュールを統一的に使うことにはならない
- ファンクタを使って作成したモジュールの関数から存在型の
type t
がリークしてもいい
1に関しては「ファンクタ内に入る時点で引数のモジュールがpackとunpackを立て続けにされる」という見方もできる。なので途中経過であるpackされた値が出てこない(結果できることの自由度は下がる)。
2についてはMulInt.mul (IntInt.create 5) 6
とMulPeano.mul (PeanoInt.create 5) 6
と、結局別モジュールであることを明示しないといけないのがポイント。
3は重要なポイントで、TaPLに載っているシステムFの存在型拡張やOCamlの第1級モジュールを使うと、最終的な戻り値がtype t
な関数は書けない。何らかの非多相的な確定した型(intとかstringとか)である必要がある。
それに対して例えばMulInt.mul
が返すのはIntInt.t
型だしMulPeano.mul
が返すのはPeanoInt.t
型である。ファンクタ内部では引数モジュールのtype t
の実装に触れることはできないが、ファンクタの外に出てしまえば作成されたモジュールはしっかり引数モジュールのtype t
にパラメータ化されている。
存在型で拡張されたシステムF、そして第1級モジュールに比べて、ファンクタはこの点に関してより自由度が高い・より表現力が強いとも言えるし、実装の隠蔽が弱いとも言える。
ファンクタの戻り値であるモジュールに対してもシグニチャを設定でき、そこで型を抽象化することもできる:
module MakeMul (M : ADDABLE_INT) : sig type t val mul : t -> int -> t end = struct type t = M.t let rec mul x n = if n = 0 then M.zero else if n = 1 then x else M.add x (mul x (n-1)) end
がこれをしてしまうと今度は引数モジュールのtype t
と戻り値モジュールのtype t
が一致しなくなってしまうので使い勝手が悪い。
utop # MulInt.mul (IntInt.create 5) 6;; Line 1, characters 11-28: Error: This expression has type int but an expression was expected of type MulInt.t
実装を隠蔽したいなら、ファンクタの方でシグニチャをいじるより、大元の引数モジュールのtype t
を抽象型にしてしまった方がいい。
次回はそのモジュールとシグニチャと存在型の話。