ja » the_basics

コメント

OCamlのコメントは(**)で囲まれた部分である。

(* これは一行だけのコメント *)

(* これは
 * 複数行に渡る
 * コメント
 *)

別の言い方をすると、コメントの仕方はCに昔からあるコメント文によく似ている。(/* ... */)

また (Perl の #... や C99, C++, Java の //... のような) 行コメントの文法はない。 ただし、何度か ##... の追加が検討されており、 私は将来 Ocaml にこれが追加されるのを期待している。

Ocaml では (* ... *) のネスト(入れ子)が許されており、 コードのある範囲をまとめてコメントアウトすることも簡単にできる。

(* このコードは作りかけ…

(* 素数テスト *)
let is_prime n =
  (* 自分へのメモ: これをメーリングリストで聞くこと *)XXX;;
*)

関数呼び出し

さて、何か関数を定義したとしよう。例えばrepeatedという関数を定義したとする。これは文字列sと整数nを引数にとり、sn回繰り返した新しい文字列を返す関数とする。

Cに由来するほとんどの言語では、この関数を呼び出すときにはこのようにする。

repeated ("hello", 3)  /* Cコード*/

これは、「関数repeatedを2つの引数により呼び出す。最初の引数は文字列helloであり、二番目の引数は3である」ことを意味する。

ところが、OCamlやその他の関数型言語では関数呼び出しを違ったように書く。特に括弧のつけ方が異なり、間違いの元になりやすい。上と同じ関数呼び出しをOCamlで書くと、

repeated "hello" 3  (* OCamlコード *)

括弧がないこと引数の間のコンマがないことに注意しよう。

ややこしいことに、repeated ("hello", 3)はOCamlでちゃんとした意味がある。これは「関数repeated一つの引数で呼び出す。引数は文字列helloと整数3の組である。」を意味する。もちろんこれは誤りである。repeatedは2つの引数を取り、しかも最初の引数は文字列であって組ではないからだ。しかしとりあえず組(tuple)については考えないことにしよう。まず引数のまわりを括弧でくくらないこと、引数をコンマで分けないことを覚えよう。

さて次に別の関数を考えよう。get_string_from_userはユーザーから文字列の入力を受け、入力された文字列を返す関数である。この関数の戻り値をrepeatedに渡したい。CとOCamlのコードを比較しよう。

/* Cコード: */
repeated (get_string_from_user ("Please type in a string."), 3)
(* OCamlコード: *)
repeated (get_string_from_user "Please type in a string.") 3

括弧のつけ方とコンマの有無に注意してほしい。一個で言うと「関数呼び出し全体を括弧でくくる。引数の周りには括弧はつけない。」ということになる。さらに例を示そう。

f 5 (g "hello") 3    (* fの引数は3つ。gは一つ *)
f (g 3 4)            (* fの引数は一つ。gは2つ。*)

# repeated ("hello", 3);;     (* エラーになる。 *)
This expression has type string * int but is here used with type string

関数定義

既存の言語で関数(Java を嗜んでいるのなら静的メソッド) を定義する方法はご存じでしょう。 では、OCaml ではどうするのでしょうか?

OCaml の文法は晴れ晴れするほどすっきりしています。 二つの浮動小数点数を引数にとって平均を計算する関数があります:

let average a b =
   (a +. b)/. 2.0;;

OCaml の"トップレベル" (Unix なら ocaml コマンドをシェルプロンプトから入力してください) にこれを入力すると、こうなります:

# let average a b =
  (a +. b) /. 2.0;;
val average : float -> float -> float = <fun>

この関数定義を良く見て、また OCaml がどういう応答を返したのかを 見るといろいろと疑問に思う点が出てくるでしょう。

次節でこれらの疑問に答えていきますが、その前にまず、 この関数と同じものを C で定義します (Java も C と似たようなものです)。 するともっと疑問が浮かぶかも知れません。 C バージョンの average:

double
average (double a, double b)
{
  return (a + b) / 2;
}

先に示した OCaml 版のとても短い関数定義と見比べましょう。 こんな疑問が浮かびませんか?:

じゃぁ答えていきましょう。

詳細は次の節、章に示します。

基本の型

OCaml の基本型は以下の通りです。

OCaml の型       範囲

int            32ビットCPU では 31ビット符号付き整数 (およそ±10億)、64ビットCPU では 63ビット符号付き整数
float          IEEE 倍精度浮動小数点数、C の double と同じ
bool           true もしくは false となる真偽値
char           8bit 文字
string         文字列
unit           () と書くもの

OCaml では整数型(int)の 1ビットを 自動的なメモリ管理(ガベージコレクション)のために内部的に使っているため、 int 型が 32ビットでなく 31 ビット(64ビット環境なら 63ビット)となっています。 この違いはごく特定のケースの除いて問題にはならないでしょう。 たとえばループの回数を数える場合、OCaml では 20 億ではなく10億までしか カウントできないことになりますが、 どんな言語でもこの限界近くでカウントするのなら、巨大数を扱える型 (OCaml なら NatBig_int モジュール)を使うべきであるので、 この制限が問題になることはないでしょう。 とはいえ、32bit 整数型を使いたいとき (例:暗号コードやネットワークスタックなどを書くとき)には、 OCaml にはあなたの環境の普通の整数型に相当する nativeint 型があります。

OCaml には符号なし整数型は基本型としてはありませんが、 nativeint 型を使えば同じことができます。 あと、OCaml には単精度の浮動小数点数は存在しません。

OCaml には文字型として char型があり、 'x' などと書きます。 残念ながら char型では Unicode、UTF-8 はサポートしていません。 これは OCaml で対応してしかるべき重大な欠陥ですが、 かわりに comprehensive Unicode libraries があります。

string 型は単なる文字のリストと言うわけではありません。 もっと効率的な内部表現で実装されています。

unit 型は C の void 型に似ていますが、 これについてはもう少し後で述べます。

明示的/暗黙の型変換

C やその派生言語では、整数型がある特定の条件で浮動小数点型に昇格します。 たとえば、1 + 2.5 と書いた場合、 第一引数の整数は浮動小数点数に昇格して、結果は浮動小数点数になります。 ((double) 1) + 2.5 と書いたのと同じ実行結果になるわけですが、 これが暗黙の内に行われます。

OCaml ではこのような暗黙の型変換は一切行われません。 OCaml で 1 + 2.5 と書くと型エラーとなります。 OCaml の + 演算子は二つの整数を引数に取るので、ここで 整数と浮動小数点数を与えられるとエラーを通知します。

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

(OCaml のエラーメッセージはフランス語から英訳されたもので、 「float になっているけど int を使うべきでは?」 「あなたは浮動小数点数を使ったがここは整数型が期待されている」 くらいの意味です。訳注:日本語を母語とする我々にはどうでもいい話ですね)

二つの浮動小数点数を足し算したい場合にはこれと違う演算子 +. を 使います (後置されているピリオドに注意)。

OCaml は自動的に int を float に昇格することはないので、次のも間違いです:

# 1 +. 2.5;;
  ^
This expression has type int but is here used with type float

OCaml は第一引数に文句をつけています。

もし本当に整数と浮動小数点を足し算したい場合、どうすれば良いでしょうか (それぞれ if という変数に格納されているとします)。 OCaml では明示的なキャストが必要です。

float_of_int i +. f;;

float_of_intint をとって float を返す関数です。 これらの関数は int_of_float, char_of_int, int_of_char, string_of_int など 全組合わせあり、概ね想像通りの動作をするはずです。

int から float への変換が特に良く出てくるので、 float_of_int 関数には短い別名が付けられており、 上の例はこのようになります。

float i +. f;;

(C と違い OCaml では型と関数が全く同じ名前です)

暗黙と明示的、どちらの型変換がよい?

これらの明示的なキャストが見苦しくて手間もかかると思うかも知れません。 確かに一理あるのですが、 明示的キャストを支持する少なくとも以下の議論があります。 一つめは、明示的な型変換を必須とすることによって、OCaml では型推論(後述) が実現できています。 型推論は明示的なキャストによる余分なタイピングを相殺して余りあるだけの すばらしい時間節約効果があります。 二つめは、C でデバッグの手間がかかっているのなら、 (a) 暗黙のキャストに起因するバグが探しづらいこと、 (b) 暗黙のキャストが何処でどう実行されたのか試行錯誤するのに時間がかかること、 に気づくべきです。 キャストを明示することでデバッグが楽になります。 三つめに、ある種のキャスト(特に int <-> float) は高価な変換操作である事実です。 型変換を明示しないことによる利益はないと思いますよ。

普通の関数と再帰関数

C などの言語と違って、 単に let ではなく let rec と明示的に書かない限り、 関数は再帰的には使えません。 再帰関数の例を示します:

let rec range a b =
  if a > b then []
  else a :: range (a+1) b
  ;;

range 関数が自分自身を呼び出していますね。

letlet rec の違いは関数名のスコープだけです。 この関数 range が単に let で定義された場合には、 range の呼び出しのところで、今定義しているところの range 関数 ではなく、既に存在するはずの(既に定義された) range 関数を探します。

letlet rec で定義された関数で性能的な違いはありません。 なので、常に let rec を使うことにすれば C のようなセマンティックになります。

関数の型

型推論があるので、関数の型を明示して書き下す必要はほとんどありません。 しかし、あなたの書いた関数を OCaml がどう推論したのかが表示されるので、 この文法を知っておく必要はあるでしょう。 関数 f が引数 arg1, arg2, ..., argnを取り、 rettype 型を返すとき、OCaml コンパイラはこう表示します:

f : arg1 -> arg2 -> ... -> argn -> rettype

矢印は変に見えるかも知れません。が、 後述する「カリー化」というものを知ると何故こう表示されるのかも納得が行くでしょう。 今はとりあえずいくつかの例を示します。

関数 repeated は文字列と数値を引数にとって文字列を返すのでした。この型は

repeated : string -> int -> string

関数 average は浮動小数点数を二つ引数にとって浮動小数点数を返すのでした。

average : float -> float -> float

OCaml 標準のキャスト関数の int_of_char は:

int_of_char : char -> int

もし関数が何も返さない (C や Java の void) のなら、 unit 型を返す、と書きます。 たとえば fputc と同等の OCaml 関数は:

output_char : out_channel -> char -> unit

多相関数

ここでちょっと変わったものを紹介します。 引数が何でも良いような関数というのはどうでしょう。

ここで引数を取るけれどそれを無視して常に3を返す変な関数があります。

let give_me_a_three x = 3;;

この関数の型はなんでしょう? OCaml では「使いたいどんな型でも」という意味の特別な記法があります。 単引用符(アポストロフィ)に文字、です。 普通、上の関数の型はこう表記されます。

give_me_a_three : 'a -> int

ここで 'a はどんな型でも良いことを意味します。 たとえばこの関数を give_me_a_three "foo" とか give_me_a_three 2.0 とか呼び出しても、 いずれも OCaml として正しい式です。

しかしこの例では多相関数が何故有用なのかは分からないでしょう。 一般的にはとても有用なのですが、それはおいおい述べようと思います。 (ヒント: 多相関数は C++ や Java 1.5 等のテンプレートに似ています)

型推論

このチュートリアルのテーマは、 関数型言語には本当にすごい特徴がいっぱいあって、 OCaml はそれらを余すところなく詰め込んだ言語になっていること、 そして、そうだからこそ、 本物のプログラマが実戦で使っている言語になっていること、です。 しかし、これら本当にすごい特徴というものの多くは、 実は「関数型プログラミング」とは関係がなかったりします。 実際、これから最初の本当に凄い特徴を述べますが。 まだ関数型プログラミングが何故「関数型」と呼ばれるのか、 ここでもまだ言及しません。 ともかく、これが最初の本当に凄い特徴です。型推論。

一言で言うと、 あなたが書いた関数や変数の型を明示的に宣言する必要がなく、 OCaml があなたのかわりにやってくれるものです。

さらに OCaml は型の整合が取れているかどうかを (別のファイルにあるものでも) チェックしてくれます。

しかし、OCaml は実用的な言語でもあるので、 型チェックをバイパスするような型システムの抜け道を用意してあります。 よほどのハッカーでもない限りこんな抜け道を使うことはないでしょうが。

さて前に出てきた average 関数に戻りましょう。

# let average a b =
  (a +. b) /. 2.0;;
val average : float -> float -> float = <fun>

摩訶不思議! OCaml は勝手にこの関数が二つの float を引数にとって float を返すと判断しました。

どうしてこんなことができるのでしょう? まず、ab が何処で使われてるのか、つまり (a +. b) を見てみます。 +. というのは常に二つの float を引数にとる単純な関数なので、 abfloat でないといけません。

次に、/.float を返す関数で、 これが average 関数の返値になるわけですから、 average 関数の返値の型もまた同じく float でないといけません。 したがって、まとめると average の型は

average : float -> float -> float

型推論はこれくらい短いプログラムだととても簡単ですが、 大きなプログラムであってもちゃんと動きます。 型推論は、 他の言語で良くありがちなセグメントフォールトや NullPointerExceptions, ClassCastExceptions (Perl だとせっかく実行時警告が出るのに無視したり…) などを引き起こすようなバグを取り除くことになるので、 これこそが最も時間の節約になる特徴となっています。