モジュール
基本的な使いかた
OCaml では、あらゆるコードがモジュールにくるまれている。 モジュール自身は別のモジュールのサブモジュールになれて、 ファイルシステムのディレクトリにとても良く似ている。 だが、こういうことはあまりしない。
プログラムを書くとき、 amodule.ml
と bmodule.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.iter
は List
モジュールの 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
でネイティブコードにコンパイルされる場合であっても、 .mli
は ocamlc
でコンパイルされる:
ocamlc -c amodule.mli
ocamlopt -c amodule.ml
...
抽象型
型定義はどうだろうか。 関数などの値はその名前や型を .mli
ファイルに書き出すことで、 エクスポートできることを見てきた。たとえば
val hello : unit -> unit
だが、モジュールはしばしば新しい型定義をする。 日付を表す簡単なレコード型を定義しよう:
type date = { day : int; month : int; year : int }
.mli
ファイルに書き出すときに二つではなく四つの選択肢がある。
- 型はシグネチャから完全に省略される
- 型定義をシグネチャにコピー&ペーストする
- 型を抽象化する: 名前だけを与える
- レコードのフィールドを読みだし専用にする:
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
ポイントは、 create
と sub
だけが 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 disjoint : t -> t -> bool
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 map : (elt -> elt) -> t -> t
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 min_elt_opt : t -> elt option
val max_elt : t -> elt
val max_elt_opt : t -> elt option
val choose : t -> elt
val choose_opt : t -> elt option
val split : elt -> t -> t * bool * t
val find : elt -> t -> elt
val find_opt : elt -> t -> elt option
val find_first : (elt -> bool) -> t -> elt
val find_first_opt : (elt -> bool) -> t -> elt option
val find_last : (elt -> bool) -> t -> elt
val find_last_opt : (elt -> bool) -> t -> elt option
val of_list : elt list -> t
val to_seq_from : elt -> t -> elt Seq.t
val to_seq : t -> elt Seq.t
val add_seq : elt Seq.t -> t -> t
val of_seq : elt Seq.t -> t
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 disjoint : t -> t -> bool
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 map : (elt -> elt) -> t -> t
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 min_elt_opt : t -> elt option
val max_elt : t -> elt
val max_elt_opt : t -> elt option
val choose : t -> elt
val choose_opt : t -> elt option
val split : elt -> t -> t * bool * t
val find : elt -> t -> elt
val find_opt : elt -> t -> elt option
val find_first : (elt -> bool) -> t -> elt
val find_first_opt : (elt -> bool) -> t -> elt option
val find_last : (elt -> bool) -> t -> elt
val find_last_opt : (elt -> bool) -> t -> elt option
val of_list : elt list -> t
val to_seq_from : elt -> t -> elt Seq.t
val to_seq : t -> elt Seq.t
val add_seq : elt Seq.t -> t -> t
val of_seq : elt Seq.t -> t
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.ml
や map.ml
を見るのがもっとも良いだろう。
最後の注釈: ファンクタは、
正しいプログラムを書くためのプログラマーの手助けとなるものであって、
性能向上のためのものではない。
ファンクタのソースコードにアクセスを必要とするのに、 ocamldefun
のようなデファンクタライザを使わなければ、 実行時のペナルティさえある。
モジュールの実用的な操作
モジュールのインタフェースの表示
ocaml
トップレベル環境では、 次のトリックで List
などの既存モジュールの中身を可視化できる。
# module M = List;;
module M = List
あるいは、ほとんどのライブラリにはオンラインドキュメントがあり、 または
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
type 'a t = 'a list = [] | (::) of 'a * 'a list
val length : 'a list -> int
val compare_lengths : 'a list -> 'b list -> int
val compare_length_with : 'a list -> int -> int
val cons : 'a -> 'a list -> 'a list
val hd : 'a list -> 'a
val tl : 'a list -> 'a list
val nth : 'a list -> int -> 'a
val nth_opt : 'a list -> int -> 'a option
val rev : 'a list -> 'a list
val init : int -> (int -> 'a) -> '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 filter_map : ('a -> 'b option) -> '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 find_opt : ('a -> bool) -> 'a list -> 'a option
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 assoc_opt : 'a -> ('a * 'b) list -> 'b option
val assq : 'a -> ('a * 'b) list -> 'b
val assq_opt : 'a -> ('a * 'b) list -> 'b option
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 sort_uniq : ('a -> 'a -> int) -> 'a list -> 'a list
val merge : ('a -> 'a -> int) -> 'a list -> 'a list -> 'a list
val to_seq : 'a list -> 'a Seq.t
val of_seq : 'a Seq.t -> 'a list
val optmap : ('a -> 'b option) -> 'a t -> 'b t
end
Extensions.List
モジュールを生成しており、 標準の List
モジュールを全部持っているのに加えて 新しい optmap
関数を持っている。
別のファイルからは、 デフォルトの List
モジュールをオーバーライドする必要があるわけだから、 .ml
ファイルの最初に open Extensions
と書く。
open Extensions
...
List.optmap ...