モジュール

基本的な使いかた

OCaml では、あらゆるコードがモジュールにくるまれている。 モジュール自身は別のモジュールのサブモジュールになれて、 ファイルシステムのディレクトリにとても良く似ている。 だが、こういうことはあまりしない。

プログラムを書くとき、 amodule.mlbmodule.ml というふたつのファイルを使うとすると、 これらのファイルそれぞれ自動的に Amodule, Bmodule という名前のモジュールと定義し、 ファイルに格納されたものはなんでもそのモジュールで提供する。

ファイル amodule.ml の中に以下のコードがある:

let hello () = print_endline "Hello"

またファイル bmodule.ml の中に以下のものがある:

Amodule.hello ()

普通、ファイルはひとつずつコンパイルされる。そうしてみよう:

ocamlopt -c amodule.ml
ocamlopt -c bmodule.ml
ocamlopt -o hello amodule.cmx bmodule.cmx

これで "Hello" と表示する素晴らしい実行可能ファイルが出来た。 お分かりのように、与えられたモジュールから何かをアクセスしたい場合、 モジュール名(常に大文字で始まる)にピリオドをつけて使いたいもの、 と用いる。 モジュールで提供される物ならば、値、型、コンストラクタ、その他なんでもよい。

標準ライブラリから始まるライブラリ群は、 モジュールの集合体を提供する。 例えば List.iterList モジュールの iter 関数を指定している。

OK。与えられたモジュールをとことん使いたいなら、 きっと中身に直接アクセスしたいだろう。 それには、open ディレクティブを使う。 我々の例では、bmodule.ml はこう書くことになる:

open Amodule;;
hello ();;

補足すると、みんな醜い";;"を避ける傾向にあるので、 より一般的にこのように書く:

open Amodule
let () =
  hello ()

それはともかく、open を使うかどうかは個人の好みの問題だ。 モジュールによっては、 他の多くのモジュールで使われているのと同じ名前を提供する。 例えば List モジュールがこれにあたる。 普通、open List はしない。 他の Printf のようなモジュールでは、 printf のように、 普通には衝突してしまわないような名前を提供する。 いたるところで Printf.printf と書かずに済むように、 ファイルの先頭に open Printf と書くのが普通の感覚だ。 (訳注: プログラミングのスタイル も参照せよ)

今言及した事を短い例で示す。

# open Printf
  let my_data = [ "a"; "beautiful"; "day" ]
  let () = List.iter (fun s -> printf "%s\n" s) my_data;;
a beautiful day val my_data : string list = ["a"; "beautiful"; "day"]

インタフェースとシグネチャ

モジュールは関数、型、サブモジュール、…といったものを、 それを用いる他のプログラムで使えるように提供する。 特別なことをしなければ、モジュールで定義される全てのものは 外からアクセス可能になる。 小さい個人のプログラムならたいていは良いのだが、 モジュール内部で使う補助関数や補助型は提供せず、 提供する意味のあるものだけをモジュールが供給するほうが良い、 と言う状況はいっぱいある。

そのためにはモジュールインタフェースを定義せねばならない。 それはモジュールの実装をマスクするように機能するだろう。 ちょうどモジュールが .ml ファイルから来ているように、 対応するモジュールインタフェースやシグネチャは .mli ファイルから来る。 ここには型と値のリストなどが含まれる。 amodule.ml ファイルを書き直そう:

# let message = "Hello"
  let hello () = print_endline message;;
val message : string = "Hello" val hello : unit -> unit = <fun>

これの通りに Amodule には以下のインタフェースがある:

val message : string
val hello : unit -> unit

他のモジュールが message の値を直接アクセスすることは大きなお世話、 と仮定しよう。 制限されたインタフェースを定義してこれを隠したい。 amodule.mli ファイルはこうだ:

val hello : unit -> unit
(** Displays a greeting message. *)

(ocamldoc がサポートしているフォーマットに基づいて、 .mliファイルにドキュメントを残すのは良い習慣だ)

.mli ファイルは、 マッチする .ml ファイルの直前にコンパイルされなければならない。 .ml ファイルが ocamlopt でネイティブコードにコンパイルされる場合であっても、 .mliocamlc でコンパイルされる:

ocamlc -c amodule.mli
ocamlopt -c amodule.ml
...

抽象型

型定義はどうだろうか。 関数などの値はその名前や型を .mli ファイルに書き出すことで、 エクスポートできることを見てきた。たとえば

val hello : unit -> unit

だが、モジュールはしばしば新しい型定義をする。 日付を表す簡単なレコード型を定義しよう:

type date = { day : int;  month : int;  year : int }

.mli ファイルに書き出すときに二つではなく四つの選択肢がある。

  1. 型はシグネチャから完全に省略される
  2. 型定義をシグネチャにコピー&ペーストする
  3. 型を抽象化する: 名前だけを与える
  4. レコードのフィールドを読みだし専用にする: type date = private { ... }

3番目の場合では以下のコードになるだろう:

type date

今、モジュールのユーザは型 date のオブジェクトを扱えるが、 レコードのフィールドには直接アクセス出来ない。 モジュールで提供される関数を使わなければならない。 モジュールが 3 つの関数、 日付を生成する関数、 二つの日付の差を計算する関数、 日付を年換算して返す関数、 を提供するとしよう。

type date
val create : ?days:int -> ?months:int -> ?years:int -> unit -> date
val sub : date -> date -> date
val years : date -> float

ポイントは、 createsub だけが date レコードを生成するのに用いられるところだ。 したがってモジュールのユーザはおかしな形式のレコードを生成することは出来ない。 実際、この実装ではレコードを使うが、これは変更可能であり、 かつこのモジュールに依存するどのコードも破壊しないことを確信する! 同じライブラリの後発バージョンが内部的にデータ構造を含む実装を変えても、 同じインタフェースを見せ続けているかぎり、 ライブラリは一貫して使える。

サブモジュール

サブモジュールの実装

example.ml ファイルは自動的に Example という名前のモジュール実装になることを見た。 このモジュールのシグネチャは自動的に派生され可能な限り公開されるか、 もしくは、example.mli ファイルに書くことで制限できる。

つまり、 ファイル内からから明示的に定義されるようなモジュールがありえる。 これは現在のモジュールのサブモジュールを作る。 次の example.ml ファイルを考えてみよう。

module Hello = struct
  let message = "Hello"
  let hello () = print_endline message
end
let goodbye () = print_endline "Goodbye"
let hello_goodbye () =
  Hello.hello ();
  goodbye ()

別のファイルから見ると、 モジュールがレベルを備えていることが明らかであり、 こう書ける:

let () =
  Example.Hello.hello ();
  Example.goodbye ()

サブモジュールのインタフェース

また、サブモジュールのインタフェースを制限できる。 これはモジュール型と呼ばれる。 example.ml ファイルでやってみよう:

module Hello : sig
 val hello : unit -> unit
end = 
struct
  let message = "Hello"
  let hello () = print_endline message
end
  
(* これで、Hello.message はどこからもアクセスできない *)
let goodbye () = print_endline "Goodbye"
let hello_goodbye () =
  Hello.hello ();
  goodbye ()

上記 Hello モジュールの定義は hello.mli/hello.ml ファイル組と等価である。 コードブロックひとつに全部書くのはエレガントではないので、 普通はモジュールとシグネチャを分割定義するのが好ましい:

module type Hello_type = sig
 val hello : unit -> unit
end
  
module Hello : Hello_type = struct
  ...
end

Hello_type は名前つきモジュール型であり、 他のモジュールインタフェース定義に再利用できる。 サブモジュールが役に立つケースはあるだろうが、 この実用性は、ファンクタで明らかになる。 これは次の節で。

ファンクタ

ファンクタはおそらく OCaml の中でもっとも複雑な特徴のひとつだが、 OCamlプログラマーとして成功するためにファンクタを広く使いこなす必要はない。 実際、あなた自身ではファンクタを定義したことはないかもしれないが、 標準ライブラリで間違いなく出会うだろう。 ファンクタは Set や Map モジュールを使う唯一の方法だが、 使うのはそんなに難しくはない。

ファンクタとは何か? なぜ必要なのか?

ファンクタは別のモジュールでパラメータ化されるモジュールであり、 関数が別の値(引数)によってパラメータ化された値であるのと同じようなものだ。

基本的には、OCaml では直接にはできないのだが、 ファンクタは値で型をパラメータ化できる。 例えば、 int n を引数にとって、 長さ n の配列だけ排他的に動作するような配列操作を集めたものを返すファンクタを定義できる。 もし間違ってプログラマがこれらの関数のどれかに普通の配列を作用させたら、 コンパイルエラーになるだろう。 もしファンクタではなく標準配列型を使うと、 コンパイラがエラーを検出できないので、 何時か分からない将来にランタイムエラーとなるだろう。 これはとてもひどい話だ。

既存のファンクタの使いかた

標準ライブラリでは Set モジュールを定義しており、 これは Make ファンクタを提供している。 このファンクタは一つの引数をとり、 (少なくとも)二つのもの -- t で与えられる要素の型と compare で与えられる比較関数 -- を提供するモジュールである。 ファンクタの要点は、 プログラマが間違えたとしても 同じ比較関数がいつも使われることを保証することである。

例えば、int の集合を使いたければこうする:

# module Int_set = Set.Make (struct
                               type t = int
                               let compare = compare
                             end);;
module Int_set : sig type elt = int type t val empty : t val is_empty : t -> bool val mem : elt -> t -> bool val add : elt -> t -> t val singleton : elt -> t val remove : elt -> t -> t val union : t -> t -> t val inter : t -> t -> t val diff : t -> t -> t val compare : t -> t -> int val equal : t -> t -> bool val subset : t -> t -> bool val iter : (elt -> unit) -> t -> unit val fold : (elt -> 'a -> 'a) -> t -> 'a -> 'a val for_all : (elt -> bool) -> t -> bool val exists : (elt -> bool) -> t -> bool val filter : (elt -> bool) -> t -> t val partition : (elt -> bool) -> t -> t * t val cardinal : t -> int val elements : t -> elt list val min_elt : t -> elt val max_elt : t -> elt val choose : t -> elt val split : elt -> t -> t * bool * t val find : elt -> t -> elt end

文字列の集合では、 標準ライブラリの String モジュールが 型 t と関数 compare を提供しているので、 さらに簡単だ。 ここまで慎重に読んでいれば、 文字列の集合の操作モジュールの生成のしかたを推測できたに違いない:

# module String_set = Set.Make (String);;
module String_set : sig type elt = String.t type t = Set.Make(String).t val empty : t val is_empty : t -> bool val mem : elt -> t -> bool val add : elt -> t -> t val singleton : elt -> t val remove : elt -> t -> t val union : t -> t -> t val inter : t -> t -> t val diff : t -> t -> t val compare : t -> t -> int val equal : t -> t -> bool val subset : t -> t -> bool val iter : (elt -> unit) -> t -> unit val fold : (elt -> 'a -> 'a) -> t -> 'a -> 'a val for_all : (elt -> bool) -> t -> bool val exists : (elt -> bool) -> t -> bool val filter : (elt -> bool) -> t -> t val partition : (elt -> bool) -> t -> t * t val cardinal : t -> int val elements : t -> elt list val min_elt : t -> elt val max_elt : t -> elt val choose : t -> elt val split : elt -> t -> t * bool * t val find : elt -> t -> elt end

(括弧が必要)

ファンクタの定義のしかた

一つの引数をとるファンクタは次のように定義できる:

module F (X : X_type) = struct
 ...
end

X はモジュールに引数として渡され、 X_type はそのシグネチャであり必須だ。

次の構文を使うと、返されるモジュールのシグネチャをつけて強制できる。

module F (X : X_type) : Y_type =
struct
  ...
end

あるいは、.mli ファイルで指定できる:

module F (X : X_type) : Y_type

全体的にファンクタの構文は把握しづらい。 標準ライブラリのソースファイル set.mlmap.ml を見るのがもっとも良いだろう。

最後の注釈: ファンクタは、 正しいプログラムを書くためのプログラマーの手助けとなるものであって、 性能向上のためのものではない。 ファンクタのソースコードにアクセスを必要とするのに、 ocamldefun のようなデファンクタライザを使わなければ、 実行時のペナルティさえある。

モジュールの実用的な操作

モジュールのインタフェースの表示

Ocaml トップレベル環境では、 次のトリックで List などの既存モジュールの中身を可視化できる。

# module M = List;;
module M : sig val length : 'a list -> int val hd : 'a list -> 'a val tl : 'a list -> 'a list val nth : 'a list -> int -> 'a val rev : 'a list -> 'a list val append : 'a list -> 'a list -> 'a list val rev_append : 'a list -> 'a list -> 'a list val concat : 'a list list -> 'a list val flatten : 'a list list -> 'a list val iter : ('a -> unit) -> 'a list -> unit val iteri : (int -> 'a -> unit) -> 'a list -> unit val map : ('a -> 'b) -> 'a list -> 'b list val mapi : (int -> 'a -> 'b) -> 'a list -> 'b list val rev_map : ('a -> 'b) -> 'a list -> 'b list val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a val fold_right : ('a -> 'b -> 'b) -> 'a list -> 'b -> 'b val iter2 : ('a -> 'b -> unit) -> 'a list -> 'b list -> unit val map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list val rev_map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list val fold_left2 : ('a -> 'b -> 'c -> 'a) -> 'a -> 'b list -> 'c list -> 'a val fold_right2 : ('a -> 'b -> 'c -> 'c) -> 'a list -> 'b list -> 'c -> 'c val for_all : ('a -> bool) -> 'a list -> bool val exists : ('a -> bool) -> 'a list -> bool val for_all2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool val exists2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool val mem : 'a -> 'a list -> bool val memq : 'a -> 'a list -> bool val find : ('a -> bool) -> 'a list -> 'a val filter : ('a -> bool) -> 'a list -> 'a list val find_all : ('a -> bool) -> 'a list -> 'a list val partition : ('a -> bool) -> 'a list -> 'a list * 'a list val assoc : 'a -> ('a * 'b) list -> 'b val assq : 'a -> ('a * 'b) list -> 'b val mem_assoc : 'a -> ('a * 'b) list -> bool val mem_assq : 'a -> ('a * 'b) list -> bool val remove_assoc : 'a -> ('a * 'b) list -> ('a * 'b) list val remove_assq : 'a -> ('a * 'b) list -> ('a * 'b) list val split : ('a * 'b) list -> 'a list * 'b list val combine : 'a list -> 'b list -> ('a * 'b) list val sort : ('a -> 'a -> int) -> 'a list -> 'a list val stable_sort : ('a -> 'a -> int) -> 'a list -> 'a list val fast_sort : ('a -> 'a -> int) -> 'a list -> 'a list val merge : ('a -> 'a -> int) -> 'a list -> 'a list -> 'a list end

あるいは、ほとんどのライブラリにはオンラインドキュメントがあり、 または labltk (OCaml の Tk GUI) に付属の ocamlbrowser が使える。

モジュールのインクルード

標準の List モジュールに、 とある関数がないのが残念だと思っていて、 モジュールの一部であるかのように使いたいとしよう。 次の extension.ml ファイルで、 include ディレクティブを用いてこれを達成できる。

# module List = struct
    include List
    let rec optmap f = function
      | [] -> []
      | hd :: tl ->
         match f hd with
         | None -> optmap f tl
         | Some x -> x :: optmap f tl
  end;;
module List : sig val length : 'a list -> int val hd : 'a list -> 'a val tl : 'a list -> 'a list val nth : 'a list -> int -> 'a val rev : 'a list -> 'a list val append : 'a list -> 'a list -> 'a list val rev_append : 'a list -> 'a list -> 'a list val concat : 'a list list -> 'a list val flatten : 'a list list -> 'a list val iter : ('a -> unit) -> 'a list -> unit val iteri : (int -> 'a -> unit) -> 'a list -> unit val map : ('a -> 'b) -> 'a list -> 'b list val mapi : (int -> 'a -> 'b) -> 'a list -> 'b list val rev_map : ('a -> 'b) -> 'a list -> 'b list val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a val fold_right : ('a -> 'b -> 'b) -> 'a list -> 'b -> 'b val iter2 : ('a -> 'b -> unit) -> 'a list -> 'b list -> unit val map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list val rev_map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list val fold_left2 : ('a -> 'b -> 'c -> 'a) -> 'a -> 'b list -> 'c list -> 'a val fold_right2 : ('a -> 'b -> 'c -> 'c) -> 'a list -> 'b list -> 'c -> 'c val for_all : ('a -> bool) -> 'a list -> bool val exists : ('a -> bool) -> 'a list -> bool val for_all2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool val exists2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool val mem : 'a -> 'a list -> bool val memq : 'a -> 'a list -> bool val find : ('a -> bool) -> 'a list -> 'a val filter : ('a -> bool) -> 'a list -> 'a list val find_all : ('a -> bool) -> 'a list -> 'a list val partition : ('a -> bool) -> 'a list -> 'a list * 'a list val assoc : 'a -> ('a * 'b) list -> 'b val assq : 'a -> ('a * 'b) list -> 'b val mem_assoc : 'a -> ('a * 'b) list -> bool val mem_assq : 'a -> ('a * 'b) list -> bool val remove_assoc : 'a -> ('a * 'b) list -> ('a * 'b) list val remove_assq : 'a -> ('a * 'b) list -> ('a * 'b) list val split : ('a * 'b) list -> 'a list * 'b list val combine : 'a list -> 'b list -> ('a * 'b) list val sort : ('a -> 'a -> int) -> 'a list -> 'a list val stable_sort : ('a -> 'a -> int) -> 'a list -> 'a list val fast_sort : ('a -> 'a -> int) -> 'a list -> 'a list val merge : ('a -> 'a -> int) -> 'a list -> 'a list -> 'a list val optmap : ('a -> 'b option) -> 'a list -> 'b list end

Extensions.List モジュールを生成しており、 標準の List モジュールを全部持っているのに加えて 新しい optmap 関数を持っている。 別のファイルからは、 デフォルトの List モジュールをオーバーライドする必要があるわけだから、 .ml ファイルの最初に open Extensions と書く。

open Extensions
...
List.optmap ...