OCamlのモジュールシグニチャ隠蔽しすぎてしまいがち問題
最近のopen recursion関連記事を書いていてふと思い出したのがこちらの記事:
モジュールを使ったプログラミングではプログラマが情報を必要以上に減らしたモジュール型を書いてしまって 型の同値性が知らず知らずに失われてしまう、ということが、ままある。
そしてこちら:
今回はこちらについて修正方法を挙げたりしながらつらつらと考えてみたい。
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.t
がint
であることをシグニチャに追加することでM.t
という型が失われてもM.e
の値がint
として型がつくようになる。
元のコードは何が問題だったかというと、let module M : S = ... in M.e
とモジュールM
(そしてその中にあるt
の定義)がlet式の中で完結するはずだったのがM.e
として外部に漏れてしまっているのがポイント。letの外ではモジュールM
が存在しないとしたら、このM.e
という値の型は何になるんだ・・・という点が問題になる(多分・・・)。
一瞬だけしか存在しないようなモジュールをlet module ... in ...
構文で作るなら、基本的に最終的に出す値の型はシグニチャでも明示する必要がある、という話。