OCaml 標準ライブラリ探訪 #0

OCaml 標準ライブラリへようこそ

OCaml 標準ライブラリは、機能が足りない、なんで dev team はライブラリを放っているんだ、とかコケにされた上に、もう我慢できないから自分で次世代標準ライブラリを作っちゃうよ、といった運動も起こっている(extlib / batteries included / Jane Street's core)といったちょっと悲しい問題があるのですが、実の所、OCaml プログラミングで一番お世話になるのがこの標準ライブラリなのです。一応、最低限の機能は提供されていますし、コメントもかなり充実しています。OCaml のバイナリパッケージをインストールするだけでソースも見ることが出来ますし(ただしパッケージがずるをして標準ライブラリソースを飛ばしていなければですが)、コード量も多すぎず、少なすぎず、 OCaml 入門の際の副教材、コード例としても最適。標準ライブラリを読みこなせば、あなたの OCaml スキルも格段にアップすること請け合いです。

と、いう訳で、この連載では OCaml の標準ライブラリである stdlib (標準ライブラリ)と otherlibs (その他。なめた名前ですね)のモジュールを一つづつ取り上げていこうと思いますよ。

対象となる読者レベルは初心者から中級者ですが、文法や型については説明しませんので全くの初心者向けではありません。既にある程度 OCaml のプログラムは見て理解できることが必要です。

探訪の中で、標準ライブラリなんかもう飽きたもんね、という上級者の方にも何か話題を提供できればと思います。そのような、ちょっと「難しい」内容はこんな囲み記事でご紹介します。

教材の準備

もしあなたがこの記事に興味があるのであれば、まず OCaml はもうインストールされていますよね。 まだであれば是非インストールしてください。インストール方法はソースからビルドしても、バイナリパッケージからインストールしても何でも構いませんが、この連載では解説しません。いろいろ検索すれば情報がありますからご自分でどうかお願いします。

教材の準備はこれで終わりです。ソースからビルドした人はソース内の stdlib/ や otherlibs/*/ ディレクトリの中に標準ライブラリのソース、 .ml/.mli ファイルがあります。パッケージでインストールした人は OCaml のライブラリディレクトリの中にソースもインストールされているはずです (サボっているパッケージがもしあったら、、、残念ですね。) OCaml ライブラリディレクトリの位置が判らない?そういう場合は、ocamlc -where コマンドを使うと、ライブラリディレクトリを教えてくれます。例えば、

$ ocamlc -where
/usr/local/lib/ocaml

だったとすると、標準ライブラリのソースは /usr/local/lib/ocaml/*.{ml,mli} にあるはずです。otherlibs のソースは、インターフェース(例えば unix.mli)などはインストールされていますが、実装(unix.ml)はされていません。ですが otherlibs のモジュールに関してはインターフェースの解説にとどめ、実装については行いませんので大丈夫です。

関数リファレンスマニュアルを読むくらいなら .mli を読もう

標準ライブラリのこの関数はどういう意味かな?こんな機能の関数は標準ライブラリに無いかな?こういう時、OCaml の関数リファレンスマニュアル (http://caml.inria.fr/pub/docs/manual-ocaml/index.html の Part IV, The Objective Caml library) を読んでもいいのですが、わざわざブラウザでアクセスしなくても実は OCaml をインストールした時点で関数リファレンスはもうみなさんの手元にそっくりそのままあるって知ってましたか? インターフェースファイル .mli がそれです。例えば、配列ライブラリのインターフェース array.mli を見てみましょう:

(***********************************************************************)
(*                                                                     *)
(*                           Objective Caml                            *)
(*                                                                     *)
(*            Xavier Leroy, projet Cristal, INRIA Rocquencourt         *)
(*                                                                     *)
(*  Copyright 1996 Institut National de Recherche en Informatique et   *)
(*  en Automatique.  All rights reserved.  This file is distributed    *)
(*  under the terms of the GNU Library General Public License, with    *)
(*  the special exception on linking described in file ../LICENSE.     *)
(*                                                                     *)
(***********************************************************************)

(* $Id: array.mli,v 1.40 2005-10-25 18:34:07 doligez Exp $ *)

(** Array operations. *)

external length : 'a array -> int = "%array_length"
(** Return the length (number of elements) of the given array. *)

external get : 'a array -> int -> 'a = "%array_safe_get"
(** [Array.get a n] returns the element number [n] of array [a].
   The first element has number 0.
   The last element has number [Array.length a - 1].
   You can also write [a.(n)] instead of [Array.get a n].

   Raise [Invalid_argument "index out of bounds"]
   if [n] is outside the range 0 to [(Array.length a - 1)]. *)

external set : 'a array -> int -> 'a -> unit = "%array_safe_set"
(** [Array.set a n x] modifies array [a] in place, replacing
   element number [n] with [x].
   You can also write [a.(n) <- x] instead of [Array.set a n x].

   Raise [Invalid_argument "index out of bounds"]
   if [n] is outside the range 0 to [Array.length a - 1]. *)

external make : int -> 'a -> 'a array = "caml_make_vect"
(** [Array.make n x] returns a fresh array of length [n],
   initialized with [x].
   All the elements of this new array are initially
   physically equal to [x] (in the sense of the [==] predicate).
   Consequently, if [x] is mutable, it is shared among all elements
   of the array, and modifying [x] through one of the array entries
   will modify all other entries at the same time.

   Raise [Invalid_argument] if [n < 0] or [n > Sys.max_array_length].
   If the value of [x] is a floating-point number, then the maximum
   size is only [Sys.max_array_length / 2].*)

...

関数リファレンスページ( http://caml.inria.fr/pub/docs/manual-ocaml/libref/Array.html )と全く同じ内容なのが判ると思います。実は標準ライブラリの関数リファレンスマニュアルは対応する .mli ファイルから ocamldoc ドキュメントジェネレータを使って自動生成された物に過ぎません。関数の事を調べるなら、ウェブブラウザを使うよりエディタを使って .mli ファイルを読んだ方が早いのです。 OCaml ではこのように .mli ファイルにはモジュールのインターフェースと共にドキュメントを添える事が推奨されていますから、新しいモジュールを使うときはまず .mli を読む癖をつけましょう。(OCaml マニュアルの関数リファレンス以外の部分はもちろんとても有用です。必要に応じて少しづつ読まれることをお薦めします。)

モジュール実装(.ml) とシグナチャー(.mli)を別ファイルとして分離する方式は、実装ファイル(.ml) 内でも sig ... end を使えばシグナチャーが書けることからすると全く無駄で不合理に見えるかもしれません。しかし、OCaml では、これを敢えて分離することでインターフェース(.mli)部分にドキュメントを書き込み、マニュアルとしてオブジェクトと共にインストールすることですっきりしたライブラリドキュメントスタイルを提供しています。(もちろん、ライブラリ作者が .mli にちゃんとコメントを書き込んでくれること前提ですが。)

インターフェースファイル .mli は SML スタイルのモジュールシステムが導入される前、まだ struct とか sig とかが書けない Caml-light の時代からありました。その遺産を受け継いでいるため .mli を無くせなかった/無くさなかったのも一因かもしれません。

.mli で判らなければ、 .ml を読もう

.mli に書かれている型や情報ではどうも理解できない場合や、そもそも .mli が提供されていないモジュールについては、実装ファイル .ml を読みましょう。運が良ければ知りたいことについてコメントがあるでしょう。コメントがなかったり、コメントがあってもさらに実装状況を知りたければ、ソースコードを読むことになります。OCaml の標準ライブラリは必ずインターフェースファイル .mli が付属しているうえ、よくドキュメントも書かれていますので、実装ファイル .ml を読むのは型や関数に対してより深い理解をしたいときだけで構いません。

インターフェース(シグナチャー)空間を探索しよう

.mliファイルや .mlファイルを読めば、そのモジュールで定義されている型や値についての情報を得ることができます。例えば、Array.iter という関数について知りたければ array.mli や array.ml を読めば型や実装が解ります。では、知りたい型や値の名前やそれが属するモジュールさえ知らない場合はどうでしょう。その場合はある程度あたりをつけてライブラリのインターフェース空間を探索しなければなりません。

インターフェース空間の探索というと何だかややこしく聞こえますが要は grep などを使って .mli ファイルを検索すればよいのです。例えば、何かの長さについて知りたければ、おそらく length という名前だろうと予測して、grep で検索するだけです:

# OCaml ライブラリディレクトリで
$ grep length *.mli
arg.mli:    space, according to the length of the keyword.  Use a
array.mli:external length : 'a array -> int = "%array_length"
array.mli:(** Return the length (number of elements) of the given array. *)
...
arrayLabels.mli:external length : 'a array -> int = "%array_length"
arrayLabels.mli:(** Return the length (number of elements) of the given array. *)
arrayLabels.mli:   The last element has number [Array.length a - 1].
...
buffer.mli:val length : t -> int
buffer.mli:   of length [n] that was allocated by {!Buffer.create} [n].
...

コメントに混じって length という名前を持つ関数の型が出てきました。コメントを避けたければ、もうちょっと賢く、シグナチャエントリのパターンを使いましょう:

$ egrep '(external|val) +length' *.mli
array.mli:external length : 'a array -> int = "%array_length"
arrayLabels.mli:external length : 'a array -> int = "%array_length"
buffer.mli:val length : t -> int
hashtbl.mli:val length : ('a, 'b) t -> int
hashtbl.mli:    val length : 'a t -> int
list.mli:val length : 'a list -> int
listLabels.mli:val length : 'a list -> int
moreLabels.mli:  val length : ('a, 'b) t -> int
moreLabels.mli:      val length : 'a t -> int
nat.mli:val length_nat : nat -> int 
nat.mli:val length_of_digit: int
printf.mli:    external length : ('a, 'b, 'c, 'd, 'e, 'f) format6 -> int
queue.mli:val length : 'a t -> int
stack.mli:val length : 'a t -> int
stdLabels.mli:    external length : 'a array -> int = "%array_length"
stdLabels.mli:    val length : 'a list -> int
stdLabels.mli:    external length : string -> int = "%string_length"
string.mli:external length : string -> int = "%string_length"
stringLabels.mli:external length : string -> int = "%string_length"
weak.mli:val length : 'a t -> int

さて、あなたの知りたい length はあったでしょうか。ウェブ上のリファレンスマニュアルと違って、簡単なコマンドラインツールを使って検索できるのが .mli の利点です。

OCamlBrowser を使ったより高度な検索

もうちょっと進んだ検索方法として、OCaml コンパイラに標準で付属している ocamlbrowser があります。これは Tcl/Tk を利用した GUI アプリケーションで、 grep 等の OCaml とは独立したツールに比べ、より柔軟な検索を行うことができます。例えば、次の画像は ocamlbrowser を使って length という名前を持つ型や値を検索した所です。

ocamlbrowser を使えば、名前が何かわかっていないけれども、型ならわかる場合にでも検索を行えます。例えば、float を第一引数に持つ関数だったが、名前を忘れた、と言う場合、検索を type モードにして、float -> _ と入力すると float -> _ にマッチした型を持つ関数やデータコンストラクタの一覧を得ることができます。例えば次の例では float_abs の型を表示させました。この浮動小数点数の絶対値を計算する関数は abs なのか、 fabs なのか abs_float なのか、それとも float_abs (正解) なのかいつも迷う困った関数です。

ocamlbrowser はその名のとおり、 .mli ファイルをハイパーリンクつきで表示させ、型名の所をクリックしてやってその定義を表示する、といったブラウジングも可能です。まだ使ったことのない方は非常に便利ですので是非一度試してみてください。(Tcl/Tk アプリケーションなので結構クセがありますけど。)

非標準ツールも使えるよ

OCaml コンパイラに付属しない非標準のツールも使用可能ですが、自分でビルドしなければいけなかったり、コンパイラのバージョンと合わないためそのままでは使えなかったり、コンパイラにパッチを当てなければいけなかったりと色々面倒ですので、ここでは名前とその機能を紹介するに止めます。

次回は Pervasives

連載第一回目は予想どうり本題まで至る事ができませんでした。次回からはモジュール紹介に移ります。まず Pervasives から。