OCaml入門(1)

このページは最後に更新されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

last mod. 2008-04-07 (月) 18:47:22
last mod. 2008-04-07 (月) 18:47:22

インタプリタ

OCaml は、バッチコンパイルによるプログラミングも可能ですが、慣れるまではインタプリタ (イントロスペクション) によって OCaml と向き合うほうが簡単です。 OCaml インタプリタは、ocaml によって起動します。

> ocaml
       Objective Caml version 3.10.2

#

通常利用する場合はパッチコンパイルが主ですが、簡単な関数を定義して、それを試してみたい場合には、インタプリタがとても有用です。

インタプリタを終了するには、EOF を送るか、#quit;; と打ち込んで下さい。

使ってみる

OCaml のインタプリタでは、;; が、一つのまとまりを区切るターミネータとして働きます。;; を書かないといつまで立っても評価されないので注意して下さい。

# 1 + 2;;

- : int = 3

上のように式をそのまま打ち込めば、評価結果が帰って来ます。ここで、int とは、1 + 2 という式の評価結果の型を表しています。

# let a = 12 * 3;;

val a : int = 36

let a = ... の形で、a に ... の評価結果を束縛することが出来ます。束縛とは、値を変数に対応付けることで、C でいうところの int a = 12 * 3; に似ています。しかし、OCaml では、いったん束縛したら、その値を変更することは出来ません。その意味で、const int a = 12 * 3 の方が近いでしょう。尚、変数は小文字で始めなければいけません。 いったん束縛すると変更できないというのは、関数型言語の特徴です。

もちろん、a の値を、次の式以降で参照することができます。

# a + 12;;

- : int = 48

let を使うと、ローカル変数も定義できます。これには、let x = e1 in e2 という形を用います。こうすると、x は e2 の中でのみ使用可能となります。

# let a = 1 in a + 1;;

- : int = 2
# a;;

Unbound value a

基本データ型

OCaml は、次の様ないくつかの基本データ型を持っています。

int整数。1, 2, -1, ...
float浮動小数点数。精度は C の double 程度。1.0, -1,2, 3E-2, ...
char文字。引用符で括って表します。'a', 'e', ...
string文字列。二重引用符で括って表します。"string", "foobar", ...
bool真偽値。true 及び false のみを持ちます。
unit() という値のみを持つ型です。C でいうところの void に相当します。
# 1;;

- : int = 1
# 1.2;;

- : float = 1.2
# 'a';;

- : char = 'a'
# "string";;

- : string = "string"
# true;;

- : bool = true
# false;;

- : bool = false
# ();;

- : unit = ()

型推論

今までの例をみると、自分で明示的に型を書いていないのがお分かりでしょう。即ち、int x = 10 の様に、自ら int と書く事は無かった筈です。しかし、let x = 10;; と書けば、OCaml は勝手に x の型が int であると推測してくれます。

これを型推論と言い、OCaml の便利な機能の一つです。

尚、型は明示的に指定することも出来ます。そのためには、: を使います。

# let x : int = 10;;

val x : int = 10

このとき間違った型を与えると、当然の如くエラーとなります。

# let x : string = 10;;

This expression has type int but is here used with type string

インタプリタは、親切にもエラーが起こっている場所に下線まで引いてくれます。

尚、変数だけでなく、演算子や関数にも型が付いていることに注意して下さい。例えば、先程何気なく使った + 演算子ですが、これは int 型の引数しか取る事が出来ません。即ち、1 + 1.0 の場合、1.0 が float 型であるので、エラーになってしまう点に注意が必要です。

# 1 + 1.0;;

This expression has type float but is here used with type int

OCaml にはコアーションは用意されていません。これは、OCaml が型推論の数学的に厳密な理論に基づいて作られているためで、型をキャストしたり、一時的に別の型とみなすという操作は OCaml では起こりません。(例外もあります。)

int, float を変換する関数に int_of_float, float_of_int (= float) がそれぞれ用意されています。float 系の演算は +, -, ... ではなく,、+., -. ,... と . を付けます。

関数

OCaml では、関数を let による束縛で生成します。

# let f x = 3 + x;;

val f : int -> int = <fun>

この例では、関数 f を、引数 x を持つ形で定義しています。また、f の型は、int -> int で、これは、"int 型の引数を一つ受け取って、int 型の値を返す" という意味です。尚、関数は小文字で始めなければいけません。

ここでも型推論が行われていることに注意して下さい。+ という演算子は int 型の値しか取る事が出来ないので、x も int でなければいけません。このようにして、f の引数である x に、int 型が与えられます。

上記の様にして型が全て決まる場合は良いのですが、実際には型が決まらない場合も存在します。例えば、let id x = x;; と定義した時、x の型はどうなるでしょうか。int でも、float でも、string でも、この式は正しいといえます。この場合、OCaml は何でも良いという意味で、型 α (表示上は 'a) を与えます。これを、多相型 と言います。

# let id x = x;;

val id : 'a -> 'a = <fun>

ここで、'a と表示された型は、同じ型であることに注意して下さい。即ち、引数に与える型と、返って来る方は同じであり、違う事はありません。違っても良い場合は、'a, 'b, 'c ... というように、別の文字を使って型が与えられます。

# let id2 x y = y;;

val id2 : 'a -> 'b -> 'b = <fun>

尚、関数の評価結果は、最後に評価された値となります。C の様に、return で戻るということはありません。

関数を適用するためには、関数の後に引数をつけて呼べば適用できます。

# id2 1 2;;

val - : int = 2

ここで、構文解析の都合上、負の値を与える場合は括弧を付けて与えて下さい。

# id2 1 (-2);;

val - : int = -2

if 文

OCaml でも、当然 if 文を使用することが出来ます。

# let f x = if x > 0 then x else -x;;

val f : int -> int = <fun>

if 文は、if condition then value1 else value2 の形で使用します。ここで、既存の手続き型言語とは違って、if 文も値を持つことに注意して下さい。(この意味で、if 文と呼ばず、if 式と呼ぶ方が正しいかも知れませんが、ここでは if 文と呼ぶ事にします。) if 文が値を持つ事によって、上記の様な関数 f を書くことができます。C の ? 三項演算子が最も近いでしょう。

if 文にも型推論は働きます。まず、condition の型は bool でなくてはいけません。C では、int 型の値でも、0 であれば false 、そうでなければ true と認められましたが、OCaml では型推論でエラーとなります。次に、value1 と value2 の型は、同じでなければいけません。もし、別の型を許してしまうと、if 文全体の型が決まらなくなってしまいます。(value1 の型を取れば良いか、value2 の型をとれば良いかが実行時まで分からないためです。)

尚、else 節は省略できますが、その場合 condition の値が false の時に if 文の型が分からなくなるので、unit 型として () を取ると定めます。即ち、if cond then value と、 if cond then value else () は、同じ意味となります。型推論のために、value の型は unit でなければならないことに注意して下さい。

再帰関数

再帰関数は、let で束縛するのではなく、let rec を用いて束縛することで実現します。

# let rec factorial x =
    if x <= 0 then 1 else x * (factorial (x - 1));;

val factorial : int -> int = <fun>

let rec で束縛した場合、関数の名前は、その定義の中で既に束縛されています。すなわち、関数定義中に、同じ関数を呼ぶことが出来ます。let で束縛した場合は、宣言が終わるまで関数の名前を束縛することが出来ません。すなわち、関数定義中に、自分自身を呼ぶ事は出来ません。

# let factorial x = if x <= 0 then 1 else x * (factorial (x - 1));;

Unbound value fuctorial

let rec で束縛出来るのは、主に関数です。通常の変数を let rec で束縛することは出来ません。例外的に無限リスト等も束縛できますが、それは後に譲ります。

ループと末尾再帰

OCaml では、ループを書く際には再帰を利用するのが普通です。例えばある処理を n 回繰り返したい場合、次の様に再帰関数を作ります。ここまでに出ていない構文がいくつかありますが、begin ... end は、いくつかの式をまとめて一つの式にするのに使われます。if 文の then 以下では C と同じ様に一つの式しかみないので begin ... end で囲むことをお勧めします。また、; でいくつかの文を並べて書くことも出来ます。このとき、expr; の expr の型が unit でなければ警告が出ます。

# let rec f n =
    let rec iter i =
      if i < n then begin
        print_int i;
        iter (i + 1)
      end
    in
    iter 0;;

val f : int -> unit = <fun>
# f 10;;

0123456789- : unit = ()

これをみて、C のプログラマは「そんなに何回も関数を呼んでループするなんて、関数のオーバヘッドがもったいない!」と言うでしょう。でも大丈夫です。OCaml には末尾呼び出し (tail call) が実装されています。今までの環境を引き継ぐことなく関数を呼び出す場合、call ではなく jmp することにより、再帰関数がアセンブリレベルでは単なるループに落ちます。

1 から n までの和を計算する場合、通常は次の様に表しがちです。

# let rec sum n =
    if n <= 0 then
      0
     else
       n + sum (n - 1)
   ;;

val sum : int -> int = <fun>
# sum 10;;

- : int = 55

しかし、これでは sum から返って来ても呼び出し側の sum で足し算の作業をしなければならないので、末尾再帰にはなりません。末尾再帰にならない場合は普通の関数コールと同じコストがかかってしまいます。これを末尾再帰にするには次の様に新しい関数を作ると良いでしょう。

iter 内で関数 iter を呼んだ後、何もしていないことに注目して下さい。すなわち、関数 iter の値が、iter の中で呼んだ iter の値と等しくなります。これが末尾再帰です。

# let sum n =
    let rec iter i s =
      if i <= 0 then
        s
      else
        iter (i - 1) (s + i)
    in
    iter n 0
  ;;

val sum : int -> int = <fun>
# sum 10;;

- : int = 55

再帰でなくても、関数の最後に別の関数を呼ぶと call ではなく jmp します。これを一般に tail call と言います。

尚、OCaml には for も while もあるにはありますが、しばらくは再帰でループするという癖を身につけて下さい。

新規 編集 添付