Arantium Maestum

プログラミング、囲碁、読書の話題

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の記事:

blog.janestreet.com

「幽霊型を使うと実行時にはゼロコストでアクセスコントロールできるよ」という話。

問題

(ちなみにこの例は完全に上記の記事から。詳しくは是非そちらをみてください)

まあ例えばあるデータの内部実装は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

sigtype 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 treadonly 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

getsetも使える。

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

隠蔽していないとうまくいかない

こちらの記事も大変参考になった:

camlspotter.hatenablog.com

モジュール外からは「幽霊型」であることが見えてはいけない!

例えばうっかり以下のように書いてしまったとする:

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という抽象型であるという情報しか与えないのが肝要。