これは ML Advent Calendar 2017 の 4日目の記事です (書き溜めていたものを公開.)
OCaml のモジュール (module
) とモジュール型 (module type
) がややこしいと思った,そんな初心を思い出して少し記事を書きました.
OCaml で簡単なプログラムは書けるけれど,モジュール化の方法がいまいち分からないという人向けです.
(本当に書きたかった話題は「ファンクターを使ったモジュール抽象化」でした.具体的には, OCaml の非同期処理のモナド Async と Lwt の両方で使えるライブラリを設計する…ということを書きたかったのですが,時間がなくなりました.そういうものに興味がある人は,例えば ocaml-cohttp をじっくり辿ってみると良いと思います. 他に,私の作りかけのライブラリ でも,base.ml においてモナドモジュールをパラメタ化したファンクタを作り,ダイレクトスタイル と Lwt の両方のインタフェースを提供しています.)
シグネチャによる型の隠蔽
例えば,次のような (純粋関数的な) スタックのリストによる実装 Stack
を考えます.
module Stack0 = struct
type 'a t = 'a list
let create () = []
let push x s = x::s (* push x s で スタック s に 値 x をプッシュ *)
let pop = function (* pop s は,スタックの最上位の値と,スタックの残りのペアを返す *)
| x::xs -> (x, xs)
| [] -> failwith "Empty stack"
end
このモジュールは,外部からは以下のような シグネチャ (モジュールのインタフェース) を持つモジュールに見えます.
module Stack0 : sig
type 'a t = 'a list
val create : unit -> 'a list
val push : 'a -> 'a list -> 'a list
val pop : 'a list -> 'a * 'a list
end
このモジュールには問題があり,型 'a Stack0.t は 'a list であると外部に漏洩しているので,利用者はモジュール化を破ることができてしまいます. 例えば,push
これを解決するには,モジュールの シグネチャ を使って Stack.t 型を隠蔽します. まず,スタックモジュールのシグネチャ STACK を module type 宣言で定義します.
module type STACK = sig
type 'a t
val create : unit -> 'a t
val push : 'a -> 'a t -> 'a t
val pop : 'a t -> 'a, 'a t
end
シグネチャは,モジュールの関数や型が外部からどのように見えるかを指定した,いわば モジュールの型 です.今後はシグネチャとかモジュール型とか呼ぶことにします.
次に,スタックモジュールの型を STACK で制限します.
module Stack : STACK (* 隠蔽 *) = struct
type 'a t = 'a list
let create () = []
let push x s = x::s
let pop = function
| x::xs -> x, xs
| [] -> failwith "Empty stack"
end
(* あるいは *)
module Stack : STACK = Stack0
これで, 'a Stack.t 型は 抽象型 として扱われ,モジュールの外側からは内部のリスト構造にアクセスできなくなります.
(OCaml でモジュールとモジュール型を分けて書かなければならないのは面倒で, DRY 原則に反しているように思えます.しかし実際問題こういった情報隠蔽を他にどうやったら書けるのか,悩ましいところです. Haskell ではnewtype や data 型のコンストラクタをモジュール外から隠蔽して使えば同様のことが実現できますが,ある関数の型の一部のみを隠蔽するといった細かい制御はできないです.そこまで細かい制御が現実に必要かどうかは,議論があり得ると思いますが)
let? val? コロン?イコール?
モジュール式の定義とモジュール型の宣言は以下の対応があります.
モジュール式 | モジュール型 |
---|---|
let x = 式 | val x : 型 |
type t = 型式 | type t [= 型式]? |
module M = モジュール式 | module M : モジュール型 |
module type S = モジュール型式 | module type S = モジュール型式 |
ネストしたモジュール式 module M = 式
に対応する宣言は module M : 型
です. 微妙に混乱ポイントかもしれません (私は何度か module type と間違えた). 実際のところ module type をモジュールの中に書くことは少ない…と思います.
構文について (sig, struct, module, module type, ...)
module
と module type
, struct ... end
と sig .. end
,=
(イコール) と :
(コロン), モジュール・モジュール型の入れ子など, OCaml のモジュール周りには confusing な部分が多々あります.
簡単にここで述べておきます.
(OCaml の構文が忌避される最大のポイントは 文の区切りがわかりづらいところだと思います.type の宣言は次の let の直前で終端,push の定義はその次の let の直前で終端… というのは,だいぶ構文に目が慣れていないと厳しそうです. また,トップレベルの let と,式としての let ... in .. 構文の違いもあります (そもそも let の入れ子自体が関数型言語以外では見られないので,エキゾチック感を助長していると思う). ほかに Haskell プログラマが OCaml でハマりやすい罠として こんな記事 を書いたこともあります.)
モジュール定義とモジュール式
新しいモジュールは module M = モジュール式
の形で定義できます.モジュール式は struct 定義 end
の形です.つまり以下は正しいモジュール定義です.
module M = struct let x = 1 end
module N = M
モジュールはネスト(入れ子)できます.
module Monad = struct
let return x = x
let bind x f = f x
module Operators = struct (* Monad.Operators *)
let (>>=) = bind
end
end
モジュールには,シグネチャの定義を含めることもできます.
module M = struct
module type S = sig
type t
end
end
モジュール定義には,上で書いた module M : シグネチャ = モジュール式
のほかに (モジュール式 : シグネチャ)
の形でもシグネチャを指定できます.
以下のモジュールは0, 1, (+) のみからなる自前の整数型 MyInt.t を提供するモジュールです.
module MyInt : sig
val t
val zero : t
val oen : t
val plus : t -> t -> t
end = struct
type t = int
let zero = 0
let one = 1
let plus = (+)
end
ここで各関数の型 int の出現を 抽象型 MyInt.t で隠蔽しています.
次のようにも書けます.
module MyInt = (struct
type t = int
let zero = 0
let one = 1
let plus = (+)
end : sig
val t
val zero : t
val oen : t
val plus : t -> t -> t
end)
汎用性のあるモジュール型であれば, 以下の MYINT のように名前をつけるのも良いです.
module type MYINT = sig
val t
val zero : t
val oen : t
val plus : t -> t -> t
end
module MyInt : MYINT = struct
type t = int
let zero = 0
let one = 1
let plus = (+)
end
その他の話題
コンパイルについて.
OCaml プログラムは,リンク時に指定した順に各モジュールが実行されます. ocamlc -o a.out x.cmo y.cmo z.cmo
とすれば,./a.out
を実行すると x.cmo
, y.cmo
, z.cmo
の順に実行されます.「モジュールを実行する」というのは違和感を感じるかもしれませんが,OCaml ではモジュール内に副作用のある式や定義 (let f = print_endline "Hello"; fun x -> x
とか) を書けるので,プログラムを開始すると各モジュールを順に実行しながらプログラムが進行するわけです.
この先の話題
ファンクタを用いた抽象化は ML系言語 (F#を除く) の一つの大きな話題です.
実用的な話題として,ファンクタを重ねたライブラリをどう構築するかという問題があります.例えば,OCaml には Lwt と Async という二つの非同期IOライブラリがあり,ダイレクトスタイルと併せて少なくとも 3通りの I/O の書き方があります. https://github.com/mirage を見ると,ファンクタを効果的に組み合わせてこれらを抽象化したライブラリがいくつか見られます.例えば cohttp https://github.com/mirage/ocaml-cohttp/ は Lwt と Async の双方で利用可能な軽量 HTTP サーバーです.
ほかに,モジュール間の 型の共有 は,古典的ですが外せない話題です.具体的には module M(N:S with type t = u) = struct .. end
の with
節です (参考). 型の共有を使えば頑健なモジュールを柔軟に組み合わせられるようになります. さらに OCaml では シグネチャ中に現れる型やモジュールの 破壊的代入 という技もあります.
OCaml の 再帰的モジュール は,多相ヴァリアントと組み合わせて Expression Problem のひとつの答え を与えてくれます.
Modular implicits は,Scala の implicit parameters の影響を受けて生まれた機能で,Haskell の型クラスのようなオーバーロードが OCaml でも使えるようになります. 残念ながら最新の OCaml には取り込まれていませんが, opam switch 4.02.0+modular-implicits
で試せます.
(以上)