OCaml プログラミング入門


とにかく使ってみる

インタープリタの起動

インストールができたら、OCaml インタープリタを起動して色々と 遊んでみましょう。 以下では、> はシェルのプロンプト、 # はインタープリタのプロンプトです。 インタープリタでは、一つの文の終わりはゼミコロン二つ ;; で表わします。 インタープリタからの返事は便宜上イタリックにします。 コメントは (**) の間に記述する 事ができ、C と違ってコメントをネストする事もできます。

let 構文で束縛されない値は 表示されて忘れ去られてしまうと考えて下さい (インタープリタからの返事の行頭が '-')。 変数に束縛された場合にはインタープリタの返事の 行頭にその変数名が来ます。

> ocaml
        Objective Caml version 3.06

#

OCaml はバイトコードインタープリタとネイティブコードコンパイラの 両方があります。学習の方法としては、わからない事があったらとりあえず インタープリタに打ってみて何を言われるか見てみる、と言うのが良いと思います。

Hello World!

どんなプログラミング言語を学ぶ時も、「Hello World!」を画面に表示する プログラムから始めるようですが、実は「画面に表示する」と言う行為は関数型 プログラミングが忌み嫌う(^^;べき「副作用」と言う部類に入るので、関数型 プログラミングがなんたるかを理解するまでは あまり教えたく無いのが本音なのですが、そんな事を言ってると初心者に「使えねー言語だな」などと思われてしまうと思うので、 とりあえずやってみます。

まずはインタープリタから:

# Pervasives.print_string "Hello World!\n";;
Hello World!
- : unit = ()

画面に、Hello World! と表示されました。その後の - : unit = () の解釈としては、- が 今評価された値の変数名 (`-' の場合は無し)、その後の unit がその値の、そして = の後の () が具体的なと言う事になります。

今度はインタープリタに与えた入力を詳しく見ていきます。 Pervasives.print_string は関数です。 この関数の型 (C で言うところのプロトタイプ?) は、 string -> unit、つまり、string (文字列) 型を引数にうけとり、 unit 型 (void みたいなもの) の値を返す関数です。"Hello World!\n" のように、ダブルクオートで囲まれた文字列は string 型の値を定義します。 関数の呼び出しは、C などとは違って引数を括弧で囲う必要がなく、 関数の後に並べるだけです。 Pervasives と言うのはモジュールの名前です。 モジュールとは、関連する関数や値を集めたもので、 print_stringPervasives モジュールの 中にあって、文字列を引数に受けとり、画面上にその文字列を出力する関数と 言う事になります。 OCaml ではこのようなモジュール名を通じて名前空間の分割をしています。

実は Pervasives モジュールは いわゆる標準ライブラリの事で、Pervasives モジュールの中の関数は 例外的に Pervasives. と書かなくても使えます。 上では説明の都合上付けました。

関数型言語の一つの特徴として、関数も普通の値もほとんど同様に扱う事ができる と言う点があります。インタープリタでこの事を見てみましょう。

# "Hello World!\n";; (* string 型の値 *)
- : string = "Hello World!\n"

# Pervasives.print_string;; (* 関数 型の値! *)
- : string -> unit = <fun>

最初の、"Hello World!" は string 型の値だと インタープリタに解釈されました。次の Pervasives.print_string言う値は、 型が string -> unit で、<fun> は、この値が関数型である事を示しています。 このように全ての値は型を持ち、関数もまた「関数型」を持つ値にすぎません。

さて、上ではインタープリタでの結果を見ましたが、コンパイルする 場合は以下の内容を hello.ml と言うファイルに保存して下さい。

(* Hello World program *)
Pervasives.print_string "Hello World!\n";;

保存したら、ocamlc hello.ml -o hello でバイトコードに (ocamlopt hello.ml -o hello でネイティブコードに) コンパイルできます。コンパイルができたら実行してみましょう。

> ocamlc hello.ml -o hello
> ./hello
Hello World!

さて、OCaml では C や C++ のように最初に実行される特定の名前の main 関数みたいなものがありません。しかし、C や C++ と違って、関数の中でも関数の 定義ができたりするので、 プログラム全体が暗黙の main 関数の中にある と言う事ができます。 よって、上の例では暗黙の main 関数の中にある式: Pervasives.print_string "Hello World!\n" が評価されて、 (式の値自体は unit 型: () であるが) その副作用で画面に Hello World! の文字が出ました。

ちょっとオマケですが、print_string があれば、print_int もあり、それぞれの基本型用の print_* 関数があります。 しかしながら、これでまとまったテキストを出力しようとしても無理があります。 実は C の printf と同じような関数 Printf.printf があるのですが、 この関数は型付規則を例外的に処理していて、私はちゃんと説明 できません (^^; が、以下のようにも使う事ができます。

# Printf.printf "%d hoge %s foo %b\n" 3 "bar" false;;
3 hoge bar foo false
- : unit = ()

Quick Sort

もう少し複雑な例を見てみましょう。quick sort を OCaml で書いてみるとどうなるかを見てみましょう。 (以下ではリストと言う基本データ構造に対する quick sort を考えます)

標準ライブラリにはもちろんソートする関数 List.sort があるので、 自分でsort関数を書く必要はほとんどありません。

let rec quicksort = function
    [] -> []     (* base case *)
  | hd::tl ->
      let (lt, gt) = List.partition (fun i -> i < hd) tl in
        (quicksort lt)@[hd]@(quicksort gt);;

要は quick sort のアルゴリズムの定義そのままで書いています。 C の qsort 関数で考えるソートとの微妙な意識の違いは、 「与えられたリスト(配列)の要素を昇順に並び換える」のではなく、 「(元のリストはそのままに)与えられたリストの要素が昇順に並んだ、新しいリストを返す」 のが目的だと言う事です。 コードの意味を詳しく見ていきましょう。

let rec quicksort = 式
quicksort と言う名前の変数を定義しようとします。 変数 quicksort が表わす値は、右辺の式を評価した値になります。 let 文もしくは let rec 文は、ある「名前」(=変数)に値を対応付けます。 rec がついているのといないのとでは、 式の中で quicksort と言う名前の変数を使った時の解釈が変わる事になります。 rec がついている場合は、式の中で quicksort は、今まさに定義している quicksort そのものを指す事になります。 rec がついていない場合は、前にどこかで定義されたであろう quicksort を指す事になります。
function パターン1 -> 式1 | パターン2 -> 式2
functionは (ここでは名前を明示しない) なんらかの値を引数として持つ関数を定義し、 その後に `|' で区切られた switch 文ならぬ、パターンマッチ文があり、 その引数がどういう形のパターンマッチしたらどういう値を返すか、 がそれぞれのパターンの -> の後に書いてあります。 今回の場合は以下に説明するように、「空リスト([])」の場合と 「先頭要素と残りのリスト」ように分解できる場合、 の二種類の場合分けがあります。
[] -> []
[] は空のリストを表わします。空のリストをソートしても空のリスト なので、そのまま空のリストが quicksort の結果となります。
hd::tl -> ...
hd::tl というパターンは、 hd と言う要素をリスト tl の先頭にくっついている、 と言う意味のパターンです。 -> 以降の式では、hd リストの先頭要素を意味し、 tl はリストの残りを意味する事になります。 一つの要素 x しか無いリスト [x] は、 x::[] と等価なのでこの場合に含まれます。
let (lt, gt) = 式 in
右辺の式で計算された結果の値を lt と gt という二つの局所変数に結びつけます。 このように、括弧「()」で囲まれてカンマ「,」で分けられた値はtupleと呼び、 C++ の STL の pair のようなデータ構造です (今回は二つですが、幾つでも「,」を続けて要素数を増やしたものも簡単に作る事ができます)。 この表現の右辺は大きさ 2 の tuple 値を返し、 左辺も実はパターンマッチの一種で、 tuple の最初の値が lt に、二番目の値が gt に結びつけられます。
List.partition (fun i -> i < hd) tl
(fun i -> i < hd) は、 引数 i が hd (上のパターンマッチで得られたリストの先頭要素) よりも小さければ true (真) を返す無名関数です。 御想像の通り、< は比較演算子で、true (真) もしくは false (偽) を返します。 List.partition は標準ライブラリの関数で、 真/偽を返す関数と (fun i -> i < hd)、リスト (tl) を受け取ります。 リストの各要素 i が関数によって i < hd かどうかが比べられて、 関数が真を返した (hd よりも小さい) 要素からなるリストと、 関数が偽を返した (hd と同じかより大きい) 要素からなるリストの (tuple) を返します。この組が上の (lt, gt) に結び付けられる事になります。
(quicksort lt)@[hd]@(quicksort gt)
最初の (quicksort lt) は、上で得られた [hd よりも小さい要素からなるリスト] に対して、 再び quicksort を呼び出しています。つまり、 quicksort 関数が正しければ hd よりも小さい要素が昇順に並んだリストが帰って来るわけです。 同様に、(quicksort gt) は [hd と同じか、より大きい要素が昇順に並んだリスト] を意味します。 [hd] は、hd と言う要素のみを含んだリストを意味ます。 @ は、リストを結合する演算子です。 と言う事で、結果としては以上の3つが昇順に並ぶ事になります。

しかし短かいですが、ちゃんとしたコードなのでしょうか? インタープリタで見てみましょう。

# let rec quicksort = function
      [] -> []     (* base case *)
    | hd::tl ->
        let (lt, gt) = List.partition (fun i -> i < hd) tl in
          (quicksort lt)@[hd]@(quicksort gt)
val quicksort : 'a list -> 'a list = <fun>

しかし、この quicksort 関数、 変数の定義をするにも、変数の「型」をまったく指定しませんでした。 良いのでしょうか? OCaml では新しい型を定義したりする場合や、 特別な場合を除いて変数の型が何であるかを指定する必要は無いのです。 コンパイラが変数がどのようにして使われているのかを調べて、勝手に推論してくれます。 今回の場合は、変数 quicksort の型は'a list -> 'a list = <fun> であると推論してくれました。つまり 'a list を受けとり、'a list を返す関数型の値です。 この 'a と言うのは、多相型、つまり、どんな型でも良いと言う事を意味しています。 ですので、int list であろうが、float list であろうが、string list であろうが、 この quicksort と言う関数が使える、と言う事になります。

# quicksort [5;4;2;-1;9;0;3];;
- : int list = [-1; 0; 2; 3; 4; 5; 9]
# quicksort ["hoge"; "foo"; "bar"; "alongstring"; ];;
- : string list = ["alongstring"; "bar"; "foo"; "hoge"]

上ではこれは比較演算子 (<) に固定されていましたが、 違う比較演算でソートができないものでしょうか。 以下のように cmp と言う新しい比較関数を引数として取るように書換えてみました。

# let rec quicksort cmp = function
      [] -> []     (* base case *)
    | hd::tl ->
        let (lt, gt) = List.partition (fun i -> cmp i hd) tl in
          (quicksort lt)@[hd]@(quicksort gt)
val quicksort : ('a -> 'a -> bool) -> 'a list -> 'a list = <fun>

# quicksort (* 文字列の長さ(だけ)でソート *)
    (fun x y -> (String.length x) < (String.length y))
    ["hoge"; "foo"; "bar"; "alongstring"; ];;
- : string list = ["foo"; "bar"; "hoge"; "alongstring"]

# quicksort (* 文字列の長さ(だけ)でソート *)
    (fun x y -> (String.length x) < (String.length y))
    [5;4;2;-1;9;0;3];;                  (* 整数には無理ってものよ *)
Characters 66-67:
    [5;4;2;-1;9;0;3];;
     ^
This expression has type int but is here used with type string

どうでしょう。関数型言語のウリの一つ、 関数が値のように簡単に、かつ型のおかげで安全に受渡しができているのがわかるでしょうか。