OCaml プログラミング入門

はじめに


何故 OCaml なのか

ML には、SML (standard ML) と言うものがありますが、何故そっちで なく OCaml を使うのか?他にも似たもの(?)としては、Haskell と言う 言語もあります。 プログラミング言語理論的にそれぞれ微妙に(どころか、かなり?)違うそうですが、私には 理論的な話は良くわからないので、プログラミング言語理論の 専門家である友人にどれが良いか聞いたところ、その友人曰く、 「色々ある中でOCaml が一番実用的な処理系」 との事でした。 ちなみに、OCaml の(主な)作者は Xavier Leroy と言う人なのですが、 この人はかなりのハッカー&スーパーマンだそうで、Linux の スレッドライブラリの実装などもしたと言う人だそうです。 そんなような理由から、なんとなく OCaml を選びました。

さて、OCaml の O は実は Objective の O で、もともと caml と呼んで いた言語に、オブジェクト指向プログラミングを支援するための拡張がな されたものです。しかしながら、このページでは オブジェクトの使用に関する情報はほとんど載せません。というのも、 私自身、自分で書いたコードにはオブジェクトを使った事が無い (使う必要が無い?) のであまり良くしらない、と言う事で最後に簡単な紹 介をするだけで終わります。

関数型言語的プログラミング

関数型言語の定義・特徴とはなんでしょうか?多くの人が色々な意見を持って いると思いますが、私の意見は:

「関数型言語的プログラミングスタイル」(後述) が簡単にできるようデザインされている
とおおまかなものにしておきたいと思います。私がやはり素晴しいと思うのは このプログラミングスタイルです。やろうと思えば C や C++ でもこの スタイルでプログラミングができるのだと思います(かなり面倒になるとは思 いますが)。以下にこのプログラミングスタイルがどういうものなのか、 私なりの見方を述べます。

C や C++, Java などの手続型(命令型)言語でプログラミングをした ことのある人が、関数型言語を習う時に最大の難関となるのはおそらく 関数型言語のプログラミングスタイルに移行する事ではないでしょうか。 命令型言語では、プログラムとは、『まずこれをこうやって、 次にこれをこうしろ、...』、という 処理を記述したものである と言えます。コンピュータはこの処理を順番にこなして行く事で計算をします。 Lisp や ML (OCaml) などの関数型言語では、どちらかと言うと、 プログラムと言うのは、『この式で定義される値は○○と名付けよう。 次にこの式は別の値が定義されて...』 という風に、数学っぽく 等式・計算式を記述したもの に近くなり ます。全ての式はある値を意味していて、 コンピュータは目標とする値を求めるために、 式を評価して行く 事で計算をします。

別の言い方をすると、関数型言語ではプログラムの『状態』と言うものをあま り意識しません。 と言うのも、関数型言語で扱う値 (やデータ構造)は、例外もありますが、 不可変 (immutable - 一度作成したものの値を変える事ができない) なものとなっています。変える事ができないので、一度定義された値は 未来永劫変わる事なく、いつ、どこで参照しようと、最初に定義した値 と同じなので、「どっかの関数で勝手に書換えられてたりしないかなぁ?」 などと心配する必要がなく、バグを減らす効果があります。 (C++ とかで変数名の宣言に constant をつけた感じ)

一度作成した値を変えられないなんてロクなプログラムにならないじゃな いかと思われるかもしれませんが、そんな事はなくて、古い値を参照しつつ、 新しい値をまた作れば良いだけです。ここらへんの事を C や C++ などの言語 で実装しようとすると、メモリの確保・解放で色々 と悩む事になると思いますが、ML(Ocaml) の場合はガーベージコレクターが解 決してくれるので心配する必要がありません。 また、安全よりも効率を優先したい場合は、やはり mutable (可変) な もののほうが若干性能が良くなります (古い値を残しておく必要が無いので)。 OCaml では文字列(string) 型や配列(array)型は mutable であり、効率的な 処理が可能となっています。 このページでは、多少クドくなるかもしれませんが、最初のうちはなるべく mutable なものを、データを書換えないスタイルで統一して 行こうと思います。そのようなスタイルのプログラミングに慣れてきたら、 どこでどう効率が落ちているか、と言う事も理解できて来ると思うので、状況 に応じて取り混ぜて行くと良いでしょう。

重要な概念&用語

C 等の手続型言語ではあまり出てこない、 関数型言語の基本的な用語を簡単に説明します。なるべく正確な説明を目指し ていますが、なにせ理論面では素人なもので、間違いもあるかもしれません。 このページではそのような意味で使っている程度で見て下さい。 また、細かい構文などの説明は本編に任せます。

副作用

関数型言語においては全ての式はある値を表現しています。 副作用とは、その値を計算するため以外に、プログラムの状態の 変化を起こすものです。 副作用の 代表的な例は「変数代入」です。例えば C で i の値が 0 の時、 i = 10; と書かれていれば、その文の実行された 前と後とでは、i の値が変わっています。 代入の他にも、文字列の表示やファイルの読み込みなども 副作用に分類されます。(標準出力や、ファイルポインタが変化する。 Haskell では...?) 副作用のあるなしでは、プログラムの正しさに関する安心感が変ってきます。 変な例ですが、例えば C++ で

  1    int * i;
  2    *i = new int(10)
  3    hoge (i);

を実行した後で、i の指す値は何でしょう? 関数 hoge に副作用があると、 *i の値が書き変えられてしまう可能性があるため、 断定する事ができません。 逆に、hoge に副作用がなければ i の指す内容は 必ず 10 になります。 関数型言語では副作用は必要最小限に留めるのが流儀で、 プログラムの可読性が増し、バグが少なくなります。

当然疑問に思うのが、「『代入は副作用だからあんまり使っちゃ 駄目』なんて、関数型言語じゃどうやってプログラミングすんの?」と言う事ですが、 もちろん似たような事ができ、「変数の束縛」と言います。

変数の束縛

変数の束縛とは、変数と言う「名前」に、 値を対応させる(結びつける)事を言います。つまり、 値を変数の名前によって参照する事ができます。 代入との違いは、副作用ではないと言う事で、破壊的では無いと言う事です。 既に束縛された変数を再び束縛した場合、元の値はそのまま残りますが、 新しい束縛が優先されるので、参照できなくなると言うだけです。 C 言語で言うと、新しい束縛を導入する事は、 '{ ... }'で新しくスコープをつくって局所変数を 定義すると言う感じでしょうか。

(* in OCaml *)
let i = 0 in         (* i という名前に 0 と言う値を結びつける *)
let i = 10 in        (* 今度は i という名前に 10 と言う値を結びつける *)
  i;;                (* i の表わす値は? *)
- : int = 10         (* ← 後の方が優先された*)

/* in C */ const int i = 0; { const int i = 10; /* 前に出てくる i には影響を与えない */ printf("%d\n", i); /* 結果は 10 を表示する */ }

OCaml では変数の束縛は (let 変数名 = 式1 in 式2) の形で行ない、 『「式2」の中に現われる「変数名」は「式1」の事だよ』 と言う意味です。束縛された変数を束縛変数、束縛されていない変数は 自由変数 と言います。

const がついている事から分かるように、関数型言語では基本的に、 変数は一度値が決まったら変更ができない、と考えて下さい。 実は、OCaml には変数の値を代入によって変更する構文もありますが、 いわゆる関数型言語のプログラミングスタイルに慣れるまでは、使用する 事をあまりお勧めしません。

型推論

OCaml は C 言語などと同様に、「静的に型付けられた」言語です。 これはどういう意味かと言うと、コンパイル時に全ての変数の型がわかって いないといけない、と言う事です。C 言語では変数を宣言する時に、その 変数がどういう型を持っているかを宣言する必要がありました。 一番最初に覚える事の1つだった事でしょう。 ところがどっこい、OCaml ではなんと型を明示的に指定する必要はありません (指定したい場合はもちろん指定できますが)。 例えば、let i = 3 + 5 in ... とした時に、i が int 型である事を 宣言する必要は無く、コンパイラが以下のように自動的に推論をしてくれます。

  1. + は int -> int -> int 型 (二つの int 型の引 数から、int 型の値を返す)である事を知っている
  2. 3 は int 型である事を知っている。(+ の 最初の引数の型に矛盾しないぞ)
  3. 5 は int 型である事を知っている。(+ の 二番目の引数の型に矛盾しないぞ)
  4. と言う事で 3+5 (つまりは + に 3 と 5 を適用したもの) は int 型である
  5. i は int 型である

型推論の結果、どんな型でもあてはまるようなものは多相型 (後述) として推論されます。また、推論を失敗する場合、つまりどんな風に型を割り当てて も、矛盾が出てしまう場合は型エラー = プログラムのどこかが間違って いる 事がわかり、コンパイルに失敗します。例えば:

# 1 + 3.2;;
This expression has type float but is here used with type int

ここで、OCaml を学ぶ際の最初のハードルが出てきます (と言っても大した 事無いですが)。+ は引数の型は両方とも int でなければなら ないのです。C 言語などでは、整数であろうが浮動小数点数であろうが、 + で足し算ができましたが、OCaml ではそうは行きません。 浮動小数点 (float) 型の足し算には +. を使わなければなりま せん。 +では int 同士しか足せないのに、 3.2 という float 型の値が使われいる事が問題となっ ています。このように float 型と int 型の足し算をしたい場合は int の値を明示的に型変換し、+. を使う必要があります。

# (float_of_int 1) +. 3.2;;
- : float = 4.2
(C 言語ではこの変換が暗黙のうちに行なわれます)。
多相型

普通、型と言えば、int や char など、特定のものを指しますが、多相型 (polymorphic type) は ぶっちゃけた話「どの型でも良いよ」と言うような型で、 α や β もしくは 'a'b で表します。多相型を使うと、型毎に別の関数を定義しなおしたり する必要が無くなります。 C++ では template で多相型実現できていますが、 そのための構文を覚えたりしないといけなかったりと、 結構面倒なのではないでしょうか。 OCaml では型推論と一緒になって、かなり自然に使えてとても便利です。 面白く無い例ですが:

# let id x = x;;     ← 関数 id は、引数 x を受け取って、x を返す
val id : 'a -> 'a = <fun>

# id 3;;
- : int = 3

# id "hoge";;
- : string = "hoge"

このように、id は引数をそのまま返す関数を定義しましたが、 どんな型の引数に対しても適用できます。

再帰

再帰的な関数とは、その関数の中で、自分自身を呼び出しているよう な関数の事を言います。(このような関数を定義する場合、OCaml では let rec ... と言う構文を使います。 例えば、階乗の計算を次のように書く事ができます。

# let rec fact x = 
      if (x <= 1) then 1 else (x * fact (x-1));;
  val fact : int -> int = <fun>
# fact 5;;
- : int = 120

関数 fact の中で、もう一回 fact を呼び出し ているのがわかります。この場合、中で呼び出している fact の結果がわから ないと、最終的に fact の結果が計算できませんが、以下のように書換えると (引数が一つ増えますが)、中で呼び出している fact は、二番目の引数 r を返しているだけで、そのまま r が結果 となっています。

# let rec fact_tail x r = 
      if (x <= 1) then r else (fact_tail (x-1) (r * x));;
  val fact : int -> int = <fun>
# fact_tail 5 1;;
- : int = 120
このような再帰のしかたを特に末尾再帰、と呼びます。 効率の追及 でもう少し詳しく述べますが、 末尾再帰な関数呼び出しは、コンパイラが綺麗に最適化してくれます。