M.Hiroi's Home Page

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

OCaml の基礎知識


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

●使ってみよう

それでは、さっそく 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 があります。

●算術演算子

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

+ 整数の加算
- 整数の減算
* 整数の乗算
/ 整数の除算
mod 整数除算の剰余
+. 実数の加算
-. 実数の減算
*. 実数の乗算
/. 実数の除算

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 になります。これを図に示すと次のようになります。

┌─────  OCaml system  ─────┐
│                                    │
│      大域変数  y                   │
│      大域変数  x ←──────┐  │
│                                │  │
│    ┌─ 関数 bar  引数 y ─┐  │  │
│    │                  ↑  │  │  │
│    │            ┌──┘  │  │  │
│    │        x + y         │  │  │
│    │        └──────┼─┘  │
│    └───────────┘      │
│                                    │
└──────────────────┘

        図 : 大域変数と局所変数

関数 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 日