Arantium Maestum

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

OCamlのモジュールシグニチャ隠蔽しすぎてしまいがち問題

最近のopen recursion関連記事を書いていてふと思い出したのがこちらの記事:

camlspotter.hatenablog.com

モジュールを使ったプログラミングではプログラマが情報を必要以上に減らしたモジュール型を書いてしまって 型の同値性が知らず知らずに失われてしまう、ということが、ままある。

そしてこちら:

no-maddojp.hatenablog.com

gist.github.com

今回はこちらについて修正方法を挙げたりしながらつらつらと考えてみたい。

OCamlのモジュールは型を抽象化して隠蔽することができる。この機能は「実装はただの文字列だけど型システム的に『サニタイズ済み』ということが保証されていて、そうでない文字列と簡単に結合できない」などと言ったコードが書けたり、と使いこなせればモジュールと型システムの強力な機能の一つとなる。

しかし、その分「隠蔽しない」という選択肢を明示的に選択・コードとして表明する必要が(モジュールレベルで少し複雑なことをしようとすると)結構頻繁に出てくる。

no_maddojpさんの:

module type S = sig type t val e : t end
let f () = (module struct type t = int let e = 14 end : S)
let module M = (val f () : S) in M.e

このコードもまさにそういう罠である。ちなみに第1級モジュールを使う必要もなく:

module type S = sig type t val e : t end
let module M : S = (struct type t = int let e = 14 end) in M.e

でもエラーになる。(OCaml 4.11では第1級モジュールを使ってもエラーメッセージがError: This expression has type M.t but an expression was expected of type M.tではなくてbut an expression was expected of type 'aになっていた)

以下のように修正したコードだとエラーは起きない:

module type S = sig type t val e : t end
module M : S = struct type t = int let e = 14 end
M.e

ただし型は<abstr>となる(抽象の意だろう)。もしこういう抽象型への隠蔽を意図していなかったのなら後々問題(というかどこかで型エラー)になってくる。

また

module type S = sig type t val e : t val to_int : t -> int end
let module M : S = (struct type t = int let e = 14 let to_int x = x end) in M.to_int M.e

module type S = sig type t val e : t end
let module M : S with type t = int = (struct type t = int let e = 14 end) in M.e

も問題ない。前者は明示的にt -> int関数を使って(実装はidと同一なのだが・・・)変換していて、後者はそもそもM.tintであることをシグニチャに追加することでM.tという型が失われてもM.eの値がintとして型がつくようになる。

元のコードは何が問題だったかというと、let module M : S = ... in M.eとモジュールM(そしてその中にあるtの定義)がlet式の中で完結するはずだったのがM.eとして外部に漏れてしまっているのがポイント。letの外ではモジュールMが存在しないとしたら、このM.eという値の型は何になるんだ・・・という点が問題になる(多分・・・)。

一瞬だけしか存在しないようなモジュールをlet module ... in ...構文で作るなら、基本的に最終的に出す値の型はシグニチャでも明示する必要がある、という話。