(Redirected from ja/modules)
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
モジュールは関数、型、サブモジュール、…といったものを、 それを用いる他のプログラムで使えるように提供する。 特別なことをしなければ、モジュールで定義される全てのものは 外からアクセス可能になる。 小さい個人のプログラムならたいていは良いのだが、 モジュール内部で使う補助関数や補助型は提供せず、 提供する意味のあるものだけをモジュールが供給するほうが良い、 と言う状況はいっぱいある。
そのためにはモジュールインタフェースを定義せねばならない。 それはモジュールの実装をマスクするように機能するだろう。 ちょうどモジュールが .ml
ファイルから来ているように、 対応するモジュールインタフェースやシグネチャは .mli
ファイルから来る。 ここには型と値のリストなどが含まれる。 amodule.ml
ファイルを書き直そう:
let message = "Hello" let hello () = print_endline message
これの通りに 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)
文字列の集合では、 標準ライブラリの String
モジュールが 型 t
と関数 compare
を提供しているので、 さらに簡単だ。 ここまで慎重に読んでいれば、 文字列の集合の操作モジュールの生成のしかたを推測できたに違いない:
module String_set = Set.Make (String)
(括弧が必要)
一つの引数をとるファンクタは次のように定義できる:
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 : 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 ... 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
Extensions.List
モジュールを生成しており、 標準の List モジュールを全部持っているのに加えて 新しい optmap
関数を持っている。 別のファイルからは、 デフォルトの List
モジュールをオーバーライドする必要があるわけだから、 .ml
ファイルの最初に open Extensions
と書く。
open Extensions ... List.optmap ...