OCamlで幽霊型を試してみた
open recursionなモジュールに型定義を持たせる(下) - Arantium Maestum
module type S = sig type 'a t = 'a list end module M = struct type 'a t = int list end
的なコードを書いて「型が合わない!」とかやって半日溶かした(まあ他にもいろいろ試していたけど)と書いた。
今見直してみたら「あれ、これってモジュールMの'a t
は幽霊型では?」と気づいた。Sの'a t
はパラメトリックな型なのでそれはもちろん合うわけがない・・・
幽霊型に関しては「左辺に出てくる型変数が右辺には出てこない」くらいしかわかっていなかったので、この機会にちょっと調べた。
参考にしたのはJane Street BlogのYaron Minskyの記事:
「幽霊型を使うと実行時にはゼロコストでアクセスコントロールできるよ」という話。
問題
(ちなみにこの例は完全に上記の記事から。詳しくは是非そちらをみてください)
まあ例えばあるデータの内部実装はint ref
なんだけどそれを隠蔽してコンストラクターゲッターセッター他諸々をモジュールとして提供したいとする:
module M : sig type t val create : int -> t val get : t -> int val set : t -> int -> unit end = struct type t = int ref let create n = ref n let get x = !x let set x n = x := n end
sig
でtype t
だけ書いてint ref
であることを隠蔽していることに注意。これで
let x = M.create 5 x := 3 (* エラー *) M.set x 3 (* これは大丈夫。ちゃんとMの提供するインターフェイスを使う必要がある *)
とできる。しかし、このままだとM.t
型を渡されたらどのコードでもその値を変更できてしまう。「値を読むことはできても書くことは許されない」データを作ることはできるだろうか?
まったく違う型を定義して、それのために別のゲッターやら何やらを定義すればできるのだが、そうすると共通の部分(この場合はデータアクセス)も新しく書き直すことになる(そしていちいち利用するところで「これは読み取り専用のデータのためのget、これは読み書き両方のデータのためのget」などと選別しなくてはいけない)。できれば共通の部分は使い回しつつアクセスをコントロールしたい。
幽霊型
というわけで幽霊型の出番:
type readwrite type readonly module M : sig type 'a t val create : int -> 'a t val make_readonly : 'a t -> readonly t val get : 'a t -> int val set : readwrite t -> int -> unit end = struct type 'a t = int ref let create n = ref n let make_readonly n = n let get x = !x let set x n = x := n end
ポイントとしては
'a t = int ref
なので実装上はなんの違いもない(なので実行時になんの変換も必要ない=ゼロコスト)readwrite t
とreadonly t
は違う型なのでreadwrite t -> ...
な関数にreadonly t
型のデータは与えられない'a t -> ...
といった多相関数ならどちらでも受け入れられるtype readwrite
type readonly
は空集合(uninhibited typesというらしい)create
関数は結果型が多相になっているので使うときに明示的に型を指定する必要がある
使い方
まずはreadonly
を試してみる:
utop # let x : readonly M.t = M.create 5;; val x : readonly M.t = <abstr> utop # M.get x;; - : int = 5 utop # M.set x 5;; Line 1, characters 6-7: Error: This expression has type readonly M.t but an expression was expected of type readwrite M.t Type readonly is not compatible with type readwrite
create
は多相なのでlet x : readonly M.t = M.create 5
のように明示的に型を指定してやる。この場合はreadonly
なのでget
はできるがset
はできない。
readwrite
で作ると:
utop # let y : readwrite M.t = M.create 5;; val y : readwrite M.t = <abstr> utop # M.get y;; - : int = 5 utop # M.set y 3;; - : unit = () utop # M.get y;; - : int = 3
get
もset
も使える。
readwrite
からreadonly
への変換:
utop # let x = M.make_readonly y;; val x : readonly M.t = <abstr> utop # let z = M.make_readonly y;; val z : readonly M.t = <abstr> utop # M.get z;; - : int = 3
隠蔽していないとうまくいかない
こちらの記事も大変参考になった:
モジュール外からは「幽霊型」であることが見えてはいけない!
例えばうっかり以下のように書いてしまったとする:
type readwrite type readonly module M : sig type s (* ここ *) type 'a t = s (* ここ *) val create : int -> 'a t val make_readonly : 'a t -> readonly t val get : 'a t -> int val set : readwrite t -> int -> unit end = struct type s = int ref (* ここ *) type 'a t = s (* ここ *) let create n = ref n let make_readonly n = n let get x = !x let set x n = x := n end
まあこの例でこんなことをするのは「うっかり」ではありえないと思うが・・・ こんなうっかりをしてしまった場合、readonly M.t = M.s = readwrite M.t
だということがM
を使う側から見えてしまうので:
utop # let x : readonly M.t = M.create 5;; val x : M.s = <abstr> utop # M.set x 3;; - : unit = ()
とアクセス制限がかからなくなってしまう。あくまで外部には'a M.t
という抽象型であるという情報しか与えないのが肝要。