Previous Up Next
Chapter 8 単純なモジュールとバッチコンパイル

Objective Caml では,バッチコンパイラ ocamlc を用いて Objective Caml プログラムを書いたファイルから,実行可能ファイルを生成することができる. また,プログラムを複数のファイルに分割してコンパイルすることも できる.このとき,分割された単位をモジュールと呼ぶ. Objective Caml ライブラリもモジュールの形で提供されており, その使用方法は同じである.

8.1 ライブラリモジュールの使い方

まずは,ライブラリの使用法を学びながら,既存のモジュールの 使い方をみてゆく.Objective Caml のライブラリは,List, Array, Sort モジュールなどのデータ構造に関するもの,Printf などの入出力に 関連するもの,Sys モジュールなどの,OSやObjective Caml 処理系とのインター フェースをとるためのもの等が豊富に用意されている.ひとつひとつの 詳しい機能などはマニュアルの19章をみてほしい.ここでは List モジュールと Queue モジュールを題材にする.

モジュール内の関数は,⟨ モジュール名 ⟩.⟨ 関数名 ⟩ と言う形で 呼び出すことができる.
# List.length [5; 6; 8];;
- : int = 3
# List.concat [[4; 35; 2]; [1]; [9; -4]];;
- : int list = [4; 35; 2; 1; 9; -4]
以前にみてきたリスト操作のための関数 rev, append, map, fold_left, fold_right 等は,ほとんど List モジュールに定義されている.

Queue モジュールは,いわゆる待ち行列の データ構造を実装したもので,リストのように同種のデータをまとめて格納するの に用いる.add, take という要素を追加,取り出す関数が 用意されているが,特徴は「先入れ先出し」であり,add した順番でしか take できない. Queue モジュールはモジュール内で t という名前の独自の型を定義している.こ の場合には,その型にもモジュール名がついた形 'a Queue.t で表される.
# let q = Queue.create ();;
val q : '_a Queue.t = <abstr>
# Queue.add 1 q; Queue.add 2 q;;
- : unit = ()
# Queue.take q;;
- : int = 1
# Queue.take q;;
- : int = 2
# Queue.take q;;
Exception: Queue.Empty.
最後に発生した例外 EmptyQueue モジュール内で定義されたものである.

open 宣言
同じモジュールを使い続けると,いちいちモジュー ル名をつけるのが面倒になってくる.open 宣言は文字通りモジュールを 「開く」もので,モジュール内の定義がモジュール名なしでアクセスできるよ うになる.
# open List;;
# length [3; 9; 10];;
- : int = 3
この open 宣言は,open した時点ですでに宣言されている同名の定義を 隠してしまうので,同名の定義を提供する複数のモジュールを開くときには 順番に注意した方がよい.隠されてしまった名前は,結局モジュール名つきの 記法でアクセスすることになる.

8.2 モジュール・インターフェース

ライブラリ・モジュールは ocamlc -v を実行して表示されるディレクトリ にソースが置いてある.例えば Queue モジュールは(最初の文字を小文字にした) queue.ml にその実装が書かれている. このディレクトリには .mli という拡張子を持つファイルも置かれている. 例えば,queue.mli を見てみると,(コメントを除くと)
type 'a t
exception Empty
val create : unit -> 'a t
val add : 'a -> 'a t -> unit
など,モジュールで提供される型,例外や関数の名前が列挙されている. また関数に関してはその型も書かれている.

このファイルは Queue モジュールの外部インターフェース(interface)を記述しており,Queue モジュールを使う場合には(その実装に関わらず)このインターフェースに 従って使用することが求められる.

「その実装に関わらず」「インターフェースに従って」という意味は, モジュールの実装とインターフェース を比べてみるとわかってくる.例えば,list.ml では,chop という関数 が定義されているのだが,list.mli にはその名前は出現しないため, List.chop という関数を使用することはできない.また,queue.ml では, 'a cell という型が定義されているが,queue.mli には出現しないため, この型の値を作ることはできない.(そもそも,そんな型が宣言されている という事実すら見えない.)このように,インターフェースは,モジュール 内部での局所的に使う関数や型を隠すために使用することができる.

この関数なり型なりを定義ごと隠してしまう,という以外に,インターフェースによる 情報隠蔽のもうひとつの形態として,「型の定義の中身」を隠す, ということができる.例えば,queue.ml では,'a t というキューのデータ構造を 表現する型がレコード型を使って定義されているが,queue.mli では
type 'a t
という = 以下がないものが書かれているだけである.このようにインターフェースに 記述することで, t という名前の型が存在することだけを外に見せ,それが実際どう定義されている のかを隠蔽することができる.こういった隠蔽により,t をあたかも, create, add などの関数をプリミティブとする基本型のように外部に見せることが可能になる.

このような情報隠蔽の仕組みは, Queue を使っているコードが実装依存ではないということを保証できるので, ソフトウェアを部品化する際に有効である. 例えば,キューを別のデータ構造を用いて実装したくなった場合にも, インターフェースさえ変わらないように気をつければ, モジュールを使っているコードを気にせずに実装を変更することができる.

8.3 バッチコンパイラによる実行可能ファイルの生成

もっとも単純な ocamlc の使用法は,Objective Caml の宣言を書いたファイル(拡張子は .ml)を用意して, シェルのコマンドラインから
ocamlc -o ⟨ 出力ファイル名 ⟩ ⟨ ソースファイル名 ⟩
としてコンパイラを 起動すると,⟨ 出力ファイル名 ⟩ という実行可能なファイルが生成される. -o オプションを省略すると a.out という名前で生成される.
igarashi@zither:text> cat hello.ml 
let _ = print_string "Hello, World!\n"
igarashi@zither:text> ocamlc hello.ml
igarashi@zither:text> a.out
Hello, World!
igarashi@zither:text> cat fact.ml
let rec fact n =
  if n = 0 then 1 else n * fact (n - 1)
let _ = print_int (fact 10)
igarashi@zither:text> ocamlc -o fact10 fact.ml
igarashi@zither:text> ./fact10 
3628800igarashi@zither:text> 
バッチコンパイルされるファイルの中には宣言の並びだけが許され, インタラクティブコンパイラで見たような,式だけからなるものは はじかれるので, let _ = ... のような,式を評価して結果を捨てる宣言として記述している. (C の main 関数に相当するものがなく,ただ単に上から評価を行っていく.) また,.ml に対応する .mli ファイルがある場合は, インターフェースに従って実装がされているかの検査なども行われる.

さて,最初に述べたようにソースファイルは複数のファイルに分割することが できる.Objective Caml システムでは,一つ一つのソースファイルがモジュールに 対応し,UNIX 上でのファイル名の先頭を大文字にしたものが,Objective Caml での モジュール名になる.例えば, foo.ml のソースファイル中の宣言は,他のファイルからは,モジュール Foo にあるものとしてアクセスされる.下は,fact.mlmain.ml である.
igarashi@zither:samples> cat fact.ml 
let rec fact n =
  if n = 0 then 1 else n * fact (n - 1)
igarashi@zither:samples> cat main.ml
(* main.ml *)
let _ = 
  print_int (Fact.fact 10);
  print_newline();
main.ml では Fact モジュール内の fact 関数を Fact.fact という 名前で使用している.コンパイラには,二つのファイル名を, 依存されているものから順に並べる.
igarashi@zither:samples> ocamlc -o fact10 fact.ml main.ml
igarashi@zither:samples> fact10
3628800
また,-c オプションを用いると,各モジュールを個別にコンパイルするこ とができる.中間的なオブジェクトファイルとして,.cmi という拡張子を 持つ,モジュールのインターフェース情報(モジュール内にどんな名前の関数 がどんな型で宣言されているかの情報・シグネチャとも呼ぶ)をコンパイルし たファイルと,.cmo という拡張子を持つモジュール自体をコンパイルした ファイルが生成される..cmi ファイルは,そのモジュールを使用するファ イルがコンパイルされるときに必要であり,(.cmo 自体は必要ではない.) 下の例で,main.ml を先にコンパイルすることはできない.(また,インターフェース定義ファイルがある場合には,それを先にコンパイルする必要がある.)そして,.cmo はあとでリンクして実行可能ファイルを生成することができる.
igarashi@zither:samples> ocamlc -c fact.ml
igarashi@zither:samples> ocamlc -c main.ml
igarashi@zither:samples> ocamlc -o fact10 fact.cmo main.cmo
各モジュールのインターフェースは -i オプションで標準出力に書き出すことがで きる.
igarashi@zither:samples> ocamlc -i -c fact.ml 
val fact : int -> int
プログラマはこれを見て,各関数に意図通りの型が与えられているかどうかを 確認することができる.




Previous Up Next