M.Hiroi's Home Page

Functional Programming

お気楽 OCaml プログラミング入門

[ PrevPage | OCaml | NextPage ]

はじめに

OCaml は ML (Meta Language) という関数型プログラミング言語の一つです。ML は 1970 年代後半に Edinburgh 大学で定理証明を行うシステム Edinburgh LCF を記述するため、R. Minler 博士を中心に開発された言語です。その後、改良が重ねられ、いくつかの ML 処理系が作られました。その中で有名なのが SML/NJ (Standard ML of New Jersey) と OCaml (Objective Caml) です。Caml はフランスの INRIA 研究所で開発された ML 処理系で、そこにオブジェクト指向機能を加えたものが OCaml です。

UNIX 系 OS の場合、OCaml のインストールはパッケージマネージャを使うと簡単です。たとえば、Ubuntu であれば以下のコマンドでインストールすることができます。

sudo apt install ocaml-nox
  or
sudo apt install ocaml

X Window System (X11) を使わない場合は ocaml-nox でかまいません。ただし、これでインストールされるのは少し古いバージョン (4.05.0, 2020 年 6 月現在) です。最新のバージョンをインストールする場合は OCaml のパッケージマネージャ OPAM を使用するようです。以下のページが参考になると思います。

Windows の場合、WSL (Windows Subsystem for Linux) もしくは Cygwin を使ったほうが簡単です。M.Hiroi は WSL で Ubuntu 18.04 を使っています。Cygwin には OCaml のパッケージが用意されているので、それをインストールするだけです。本ページは WSL (Ubuntu 18.04) で OCaml を動かすことにします。

●OCaml の特徴

関数型言語というと Lisp (Common Lisp, Scheme) が有名です。Lisp の場合、データに型はありますが、変数に型はありません。これに対し ML は強く型づけされた言語で、コンパイル時に静的な型チェックを行うことで、多くのエラーを検出することができます。

ML で一番有名な機能は「型推論」でしょう。ML はプログラムから変数などのデータ型を見つけてくれるので、プログラマが型を宣言する必要はほとんどありません。推論できない場合にかぎり、ML は型宣言を要求します。この機能により、ML は静的な型チェックを行う「型付きの言語」でありながら、 Lisp のような柔軟なプログラミングが可能になっています。

この他にも、パターンマッチング、多相型関数、モジュールなど、ML には興味深い機能がたくさんあります。なお、このような特徴は SML/NJ と OCaml で大きな違いはありませんが、文法面ではかなりの違いがあります。ご注意ください。

●プログラムの実行

OCaml はプログラムをコンパイルしてから実行します。OCaml のコンパイラは対話式コンパイラとバッチコンパイラの二種類があります。たとえば、シェルで対話式コンパイラ (ocaml) を起動すると、メッセージとプロンプト # が表示されます。この状態で OCaml のプログラムを入力して簡単に実行することができます。終了する場合は #quit;; と入力してください。

なお、ocaml にはコマンドラインの編集やコマンド履歴をたどる機能がありません。この場合、rlwrap をインストールすると便利になります。

sudo apt install rlwrap

あとはシェルで rlwrap ocaml を実行するだけです。簡単なプログラムであれば、これだけでもかなり便利になります。

バッチコンパイラはプログラムをコンパイルして実行可能ファイル (executable file) を生成します。バッチコンパイラは、プログラムをバイトコードにコンパイルする ocamlc と、ネイティブコード (機械語) にコンパイルする ocamlopt の二種類があります。バイトコードにコンパイルされたファイルは、バイトコードインタプリタによって実行されますが、ネイディブコードにコンパイルされたファイルは単独で実行することができます。

●簡単なベンチマーク

OCaml はネイティブコードにコンパイルすると、当然ですがプログラムを高速に実行することができます。また、バイトコードにコンパイルする場合でも、スクリプト言語やバイトコードにコンパイルする Lisp 処理系よりも速いようです。そこで、たらいまわし関数を使って実行速度を比較してみました。

リスト : たらいまわし関数

let rec tak x y z = 
  if x <= y then z
  else
    tak (tak (x - 1) y z) (tak (y - 1) z x) (tak (z - 1) x y)

(* 時間計測 *)
let () = 
  let a = Sys.time () in
    ignore (tak 22 11 0);
    print_float (Sys.time () -. a)

(* ... *) はコメントを表します。OCaml の場合、コメントは入れ子になってもかまいません。ファイル名を tak.ml とすると、コンパイルは次のように行います。

$ ocamlc -o tak tak.ml

$ ./tak

これで実行ファイル tak が作成されます。あとはそのまま実行するだけです。自動的にバイトコードインタプリタが呼び出されてプログラムが実行されます。ocamlopt の場合も同様にコンパイルすることができます。

それでは実行結果を示します。tak 22 11 0 を計算しました。

表 : tak 22 11 0 の結果
処理系
Python (ver 3.6.9)69.6
Lua (ver 5.3.3)37.2
Ruby (ver 2.5.1p57)25.4
Gauche (ver 0.9.9)25.3
ocamlc (ver 4.05.0)10.8
SBCL (ver 1.4.5)6.38
LuaJIT (var 2.1.0-β)4.02
Julia (ver 1.3.0)2.02
SBCL (最適化)1.57
GCC -O2 (ver 7.4.0)1.23
ocamlopt (ver 4.05.0)0.97

ocamlopt が gcc よりも速いとは驚きました。gcc のコンパイルオプションは -O2 を指定しただけなので、他のオプションを指定するともう少し速くなるかもしれません。興味のある方は試してみてください。

バイトコードにコンパイルする場合でも、OCaml は他の処理系より高速です。たらいまわし関数のように、再帰呼び出しの回数が多いテストは関数型言語に有利だったかもしれません。そうだとしても、OCaml のコンパイラは優秀だと思います。

●Enjoy Programming!!

M.Hiroi は OCaml でプログラミングするのは初めてです。このページで簡単なプログラムを作りながら OCaml を勉強していきたいと思っております。たいしたことはできませんが、よろしくお付き合いくださいませ。


初版 2008 年 6 月 7 日
改訂 2020 年 6 月 21 日

OCaml の基礎知識

●使ってみよう

それでは、さっそく OCaml を使ってみましょう。Unix 系 OS の場合、シェル上で ocaml を実行すると、対話モード (interactive mode) で OCaml を起動することができます。

$ rlwrap ocaml
        OCaml version 4.05.0

#

# は Ocaml のプロンプトです。終了する場合は #quit;; と入力してください。Unix 系 OS の場合、Ctrl-D (Ctrl キーを押しながら d を押す) を入力しても終了します。プロンプトのあとに式を入力すると、OCaml は式を評価して結果を返します。

# 1 + 2 * 3;;
- : int = 7
# -3 * 4;;
- : int = -12

対話モードで式を入力する場合、最後にセミコロンを 2 つ (;;) 入力してからリターンキーを押します。;; が入力終了のしるしになります。1 + 2 * 3 の結果を見ると、値が 7 でデータの種類が int であることがわかります。データの種類や種別のことを「データ型」、またはたんに「型」といいます。

負の数を表す場合、OCaml は普通の数式と同じく - を使います。SML/NJ の場合は - ではなくチルダ ( ~ ) を使います。OCaml は -3 * 4 のようにマイナス記号を使うことができますが、SML/NJ の場合は ~3 * 4 となります。ご注意ください。

●整数と実数

OCaml の場合、int は整数を表すデータ型で、float が実数を表すデータ型です。整数は 10 進数で表しますが、先頭に 0b (0B) を付けると 2 進数、0o を付けると 8 進数、0x を付けると 16 進数で表すことができます。

整数の範囲は、64 bit CPU の処理系で -4611686018427387904 (-262) から 4611686018427387903 (262 - 1) になります。これらの値は min_int, max_int として定義されています。また、標準ライブラリ (モジュール) Int32 と Int64 を使うと、32 bit 整数と 64 bit 整数を扱うことができます。

OCaml の実数は IEEE754 形式という倍精度浮動小数点数で表されていて、正の最大値と最小値は max_float と min_float に定義されています。それから、正の無限大を表す infinity, 負の無限大を表す neg_infinity, 実数でないことを表す nan があります。

●算術演算子

ここで、よく使われる算術演算子をまとめておきましょう。

OCaml の場合、整数と実数 (浮動小数点数) で用いる演算子が異なります。実数の演算子は後ろにドット ( . ) を付けます。OCaml は型を厳密にチェックするプログラミング言語なので、整数と実数を混在させて計算することはできません。また、OCaml は英大文字と英小文字を区別するので、mod は小文字で入力してください。当然ですが、数式にはカッコ ( ) を使うことができます。

# 1.0 +. 2.0;;
- : float = 3.
# -3 * (5 - 2);;
- : int = 9

整数と実数の変換は関数 int_of_float, float_of_int を使います。

# int_of_float 1.5;;
- : int = 1
# float_of_int 1;;
- : float = 1.

●文字と文字列

一つの文字を表すデータ型を文字型 (char) といいます。OCaml の場合、文字は ASCII コードのみで、日本語 (漢字やカナなど) を文字として扱うことはできません。文字は 'a' のように引用符 ' で囲んで表します。' を表す場合はエスケープシーケンスを使います。

# 'a';;
- : char = 'a'
# '\'';;
- : char = '\''
# '\\';;
- : char = '\\'

文字と整数の変換には関数 int_of_char, char_of_int を使います。

# int_of_char 'a';;
- : int = 97
# char_of_int 97;;
- : char = 'a'

文字列 (string) は "foo" や "bar" のように二重引用符 ( " ) で囲みます。C言語と同様にエスケープシーケンスを使うことできます。たとえば、\n が改行で \t がタブになります。

# "foo";;
- : string = "foo"
# "bar";;
- : string = "bar"
# "foo" ^ "bar";;
- : string = "foobar"
# "foo".[0];;
- : char = 'f'

文字列は演算子 ^ で連結することができます。それから、文字列.[n] という形式で、文字列から n 番目の文字を取り出すことができます。

●比較演算子

比較演算子は =, <>, <, >, <=, >= があります。値が等しいかチェックする述語が = で、等しくないかチェックする述語が <> です。簡単な例を示しましょう。

# 1 = 1;;
- : bool = true
# 1 <> 1;;
- : bool = false
# 1 <> 2;;
- : bool = ture
# 1 < 2;;
- : bool = true
# 1 > 2;;
- : bool = false
# "foo" = "foo";;
- : bool = true
# "foo" = "bar";;
- : bool = false

OCaml は真・偽を型 bool で表します。true が真で false が偽になります。比較演算子は整数や実数だけではなく、文字や文字列にも適用することができます。

●論理演算子

OCaml には not, &&, || という論理演算子があります。

簡単な例を示します。

# 1 < 2 && 3 < 4;;
- : bool = true
# 1 < 2 && 3 > 4;;
- : bool = false
# 1 < 2 || 3 > 4;;
- : bool = true
# 1 > 2 || 3 < 4;;
- : bool = true
# 1 > 2 || 3 > 4;;
- : bool = false

●条件分岐

条件分岐は if-then-else を使います。if E then F else G は最初に E を評価して、結果が真 (true) であれば式 F を評価し、偽 (false) であれば式 G を評価します。式 F または式 G の評価結果が if の返り値になります。式 F と G の返り値はどんな型でもかまいませんが、同じ型でなければいけません。型が違うとエラーになります。

簡単な例を示しましょう。

# if 1 < 2 then 3 * 4 else 5 * 6;;
- : int = 12
# if 1 > 2 then 3 * 4 else 5 * 6;;
- : int = 30

OCaml の場合、if-then-else の else は特別な場合を除き省略することができません。ご注意ください。

●変数

変数 (variable) は let 式で宣言します。

let 名前 = 式

Lisp などの関数型言語では、変数に値を割り当てることを「束縛 (binding)」といいます。純粋な関数型言語の場合、束縛された変数は値を書き換えることができません。手続き型言語は代入により変数の値を書き換えることができますが、純粋な関数型言語に代入操作はありません。ちなみに、Lisp は不純な関数型言語なので、変数の値を書き換えることができます。

名前 (識別子) は、先頭が英小文字またはアンダースコア ( _ ) で、そのあとに英大文字、英小文字、数字、アポストロフィ ( ' )、アンダースコアが続きます。英大文字で始まる名前は「コンストラクタ」、アポストロフィから始まる名前は「型変数」になるため、変数名や関数名として用いることはできません。ご注意ください。また、OCaml は英大文字と英小文字を区別するので、たとえば foo と fOO は異なる名前になります。コンストラクタと型変数はあとで詳しく説明します。

OCaml の場合、let で宣言された変数は、値を書き換えることはできません。ただし、OCaml には「配列 (array)」や「参照 (reference)」といった値を書き換えることができるデータ型も用意されています。

簡単な例を示しましょう。

# let a = 10;;
val a : int = 10
# a;;
- : int = 10
# let b = 2.0;;
val b : float = 2.
# b;;
- : float = 2.
# let c = "foo";;
val c : string = "foo"
# c;;
- : string = "foo"

対話モードの場合、変数名を入力するとその値が表示されます。なお、OCaml は同じ名前の変数を再定義することができます。

# a;;
- : int = 10
# let a = "foo";;
- : string = "foo"
# a;;
- : string = "foo"

トップレベルで変数を再定義すると、元の変数は隠蔽されて値を参照することができなくなります。

●組

OCaml は複数の型を組み合わせて新しい型を定義することができます。OCaml の場合、新しい型の定義方法はいくつかあるのですが、もっとも簡単で重要な方法が「組 (tuple)」です。組は複数のデータや式をカンマ ( , ) で区切り、カッコ ( ) で囲んで表します。次の例を見てください。

# let a = (1, 2);;
val a : int * int = (1, 2)
# let b = (10, 20.5);;
val b : int * float = (10, 20.5)
# let c = (1, 2.5, "foo");;
val c : int * float * string = (1, 2.5, "foo")
# let d = (1+2, 3*4);;
val d : int * int = (3, 12)

変数 a の組 (1, 2) は整数を 2 つ持っていて、型は int * int になります。このような型を「積型」といいます。積型は複数の型をアスタリスク ( * ) でつなげて表します。変数 b の組 (10, 20.5) は整数と実数なので int * float になります。変数 c の組 (1, 2.5, "foo") は int * float * string になります。また、最後の例のようにカッコの中に式を書くと、それを評価した値が組の要素になります。

組は入れ子にしてもかまいません。次の例を見てください。

# let a = ((1, 2), 3);;
val a : (int * int) * int = ((1, 2), 3)
# let b = (1, (2, 3));;
val b : int * (int * int) = (1, (2, 3))

変数 a の組は、第 1 要素が int * int の組で、第 2 要素が int です。これを (int * int) * int と表します。変数 b の組は、第 1 要素が int で第 2 要素が int * int の組になります。これを int * (int * int) と表します。どちらの組も 3 つの整数が含まれていますが、型は異なることに注意してください。

組から要素を取り出すには、「パターンマッチング (pattern matching)」という機能を使います。次の例を見てください。

# let (a, b) = (1, 2);;
val a : int = 1
val b : int = 2
# let (a, b) = ((1, 2), 3);;
val a : int * int = (1, 2)
val b : int = 3
# let ((c, d), e) = ((1, 2), 3);;
val c : int = 1
val d : int = 2
val e : int = 3

let 式の右辺 (a, b) がパターンを表します。要素が 2 つ並んでいるので、2 要素の組を表すパターンになります。パターン (a, b) と左辺の (1, 2) を照合して、変数部分に対応する要素を取り出します。そして、変数をその値に束縛します。次の例のように、(a, b) と ((1, 2), 3) を照合すると、a は (1, 2) になり、b は 3 になります。

パターンは入れ子にしてもかまいません。((c, d), e) と ((1, 2), 3) を照合すると、c = 1, d = 2, e = 3 となります。このように、パターンを使って組の要素を取り出すことができます。ただし、型が違うと照合に失敗してエラーになるので注意してください。

●関数

OCaml は関数も let で定義します。

let 名前 引数 = 式

let のあとに名前と引数を書き、= のあとに引数を含む式を書きます。たとえば、引数を 2 倍する関数 times2 を定義すると次のようになります。

# let times2 x = x * 2;;
val times2 : int -> int = <fun>
# times2 4;;
- : int = 8

関数型言語の場合、関数もデータ型の一つです。let で指定した名前が times2 であれば、変数 times2 の値は関数型のデータになります。<fun> は値が関数であることを表し、型は "引数の型 -> 返り値の型" で表します。この型を見ると、関数 times2 は引数に int をひとつ取り、int を返すことがわかります。

ここで、引数や返り値の型を指定しなくても、OCaml が型を決めていることに注意してください。この機能を「型推論」といいます。times2 は引数と整数 2 の乗算を行っているので、引数は int で返り値も int になるはずです。このように OCaml が型を推論してくれるので、私達が型を指定しなくてもプログラムすることができます。

複数の引数を持つ関数を定義する場合は組を使うと簡単です。次の例を見てください。

# let f (x, y) = 2 * x + 3 * y;;
val f : int * int -> int = <fun>
# f (1, 2);;
- : int = 8

関数 f は 2 つの引数 x, y を受け取ります。ここで関数 f の型 int * int -> int を見てください。引数の型が int * int の積型になっていますね。実をいうと、OCaml の関数は引数を一つしか受け取ることができません。複数の引数は組にして関数に渡します。つまり、関数呼び出し f (1, 2) は、組 (1, 2) に関数 f を適用するという意味なのです。

組を使えば複数の値を返す関数も簡単に作ることができます。次の例を見てください。

# let foo (x, y) =
  if x = y then (0, 0)
  else if x < y then (-1, y - x)
  else (1, x - y);;
val foo : int * int -> int * int = <fun>
# foo (10, 20);;
- : int * int = (-1, 10)
# foo (20, 10);;
- : int * int = (1, 10)
# foo (10, 10);;
- : int * int = (0, 0)

関数 foo は引数 x と y の差分の絶対値を計算し、符号とその値を返します。if-then-else は else if でつなぐことができます。x = y ならば (0, 0) を返します。x < y ならば (-1, y - x) を返し、x > y ならば (1, x - y) を返します。このように、組を使って複数の値を返すことができます。

●局所変数と大域変数

関数の引数は「局所変数 (local variable)」として扱われます。局所変数は「有効範囲 (scope : スコープ)」が決まっています。引数の有効範囲は、関数が定義されている式の中だけです。次の例を見てください。

# let x = 10;;
val x : int = 10
# let y = 20;;
val y : int = 20
# let bar y = x + y;;
val bar : int -> int = <fun>
# bar 100;;
- : int = 110

局所変数として定義されていない変数は「大域変数 (global variable)」になります。大域変数はどこからでも値を参照することができます。対話モードで変数を定義すると、それらの変数は大域変数になります。最初に定義した変数 x と y は大域変数です。

関数 bar の引数は y で、式は x + y です。関数を呼び出す場合、引数用に新しいメモリを割り当て、そこに与えられた値で引数を束縛します。大域変数 y と引数 y は同じ名前ですが、異なる変数になるのです。そして、局所変数が定義されていれば、その値が参照されます。局所変数が定義されていない場合、大域変数の値が参照されます。したがって、式の中の y は引数 y を参照し、bar の引数に x がないので、式の中の x は大域変数 x を参照します。よって、bar 100 は 10 + 100 = 110 になります。これを図に示すと次のようになります。

関数 bar を実行するとき、関数 bar の枠が作成されると考えてください。このとき、引数用に新しいメモリが割り当てられ、新しい局所変数 y が作成されるわけです。関数の実行が終了すると枠が壊されて、作成された局所変数も廃棄されます。関数 bar の場合、引数 y が廃棄されるので、対話モードでは大域変数 y の値を参照することができます。このように、関数の引数は関数定義されている式の中だけ有効なのです。

ところで、関数の中で引数以外の局所変数を定義できると便利です。OCaml の場合、let 式で局所変数を定義することができます。

let 変数 = 式1 in 式2

この let 式は、最初に 式1 を評価します。そして、変数をその結果に束縛して、式2 を評価します。その評価結果が let 式の返り値になります。なお、式1 や式2 が let 式でもかまいません。また、let 式を使って局所的な関数を定義することもできます。変数の有効範囲は let 式の中だけ、つまり式 2 の中だけになります。

たとえば、2 点間の距離を求める関数 distance を作ってみましょう。次のリストを見てください。

リスト : 2 点間の距離を求める (1)

let distance ((x1, y1), (x2, y2)) =
  let dx = x1 -. x2 in
  let dy = y1 -. y2 in
  sqrt (dx *. dx +. dy *. dy)

点の座標は組 (x, y) で表します。引数として 2 つの組 (x1, y1), (x2, y2) を受け取ります。x 座標の差分を局所変数 dx に、y 座標の差分を局所 dy に求めます。あとは、√(dx *. dx +. dy *. dy) を計算するだけです。

簡単な実行例を示しましょう。OCaml の対話モードにはディレクティブというコマンドがあり、#use "filename" でソースファイルを読み込むことができます。たとえば、ファイル名が distance.ml とすると、対話モードで次のように入力します。

# #use "distance.ml";;
val distance : (float * float) * (float * float) -> float = <fun>

読み込んだプログラムはコンパイルされて、関数 distance の型が表示されます。これで distance を呼び出すことができます。

# let p1 = (0.0, 0.0);;
val p1 : float = (0., 0.)
# let p2 = (10.0, 10.0);;
val p2 : float = (10., 10.)
# distance (p1, p2);;
- : float = 14.142135623730951

座標を表す組を変数に定義して、それを distance に渡します。すると、パターンマッチングにより、組の要素が取り出されて変数 x1, y1, x2, y2 にセットされます。

また、let 式は次のように and を使って複数の変数を定義することもできます。

リスト : 2 点間の距離を求める (2)

let distance ((x1, y1), (x2, y2)) =
  let dx = x1 -. x2 and dy = y1 -. y2 in
  sqrt (dx *. dx +. dy *. dy)

OCaml の場合、and は論理演算子ではありません。ご注意くださいませ。

●問題

次に示す関数または定数を定義してください。

  1. 実数 x を 2 乗する関数 square
  2. 円周率 pi (3.14159265359)
  3. 円の面積を求める関数 circle_area r
  4. 二つの引数の平均値をとる関数 medium (a, b)
  5. 二つの引数の二乗の平均値をとる関数 square_medium (a, b)













●解答

# let square x = x *. x;;
val square : float -> float = <fun>
# square 2.0;;
- : float = 4.
# square 2.5;;
- : float = 6.25

# let pi = 3.14159265359;;
val pi : float = 3.14159265359

# let circle_area r = pi *. square r;;
val circle_area : float -> float = <fun>
# circle_area 10.0;;
- : float = 314.159265359

# let medium (a, b) = (a +. b) /. 2.0;;
val medium : float * float -> float = <fun>
# medium (2.0, 4.0);;
- : float = 3.
# medium (2.0, 3.0);;
- : float = 2.5

# let square_medium (a, b) = medium (square a, square b);;
val square_medium : float * float -> float = <fun>
# square_medium(2.0, 3.0);;
- : float = 6.5
# square_medium(1.5, 2.5);;
- : float = 4.25

初版 2008 年 6 月 7 日
改訂 2020 年 6 月 21 日

Copyright (C) 2008-2020 Makoto Hiroi
All rights reserved.

[ PrevPage | OCaml | NextPage ]