制御構造

入出力など,副作用のある計算をするときには式を評価する順番が重要になる。OCaml にもそのための制御構造(control structure)がある。

逐次実行

1つの方法は let ~ in を使うこと。let 以下が評価された後に in 以下が評価される。

# let () = print_string "Hello, " in
print_string "world.\n";;
Hello, world.
- : unit = ()

複数の式を ; で区切って書くと左から実行する。全体の値はいちばん右の式の値。途中の式の値は捨てられる。

# print_string "Hello, "; print_string "world.\n";;
Hello, world.
- : unit = ()

条件分岐

if をつかう。then 節が unit型の式であるときに限って,else 以下を省略できる。

# if true then print_string "Hello, world.\n";;
Hello, world.
- : unit = ()

これは,条件が偽なら何もしない,ということ。

# if false then print_string "Hello, world.\n";;
- : unit = ()

begin ~ end

; と if では if のほうが結合強度が強く,then節や else節の途中で ; が出てくるとそこでif式全体が終わりだと判断される。

# let f b = if b then print_string "Hello, "; print_string "world.\n";;
val f : bool -> unit = <fun>

この関数は引数(=ifの条件)が偽なら “world.\n” だけが出力される(引数に関係ないから)。

# f true;;
Hello, world.
- : unit = ()
# f false;;
world.
- : unit = ()

もし,真の時に “Hello, world\n” を出力し,偽の時には何もしたくないなら括弧で囲む。

# let f2 b = if b then (print_string "Hello, "; print_string "world.\n");;
val f2 : bool -> unit = <fun>
# f2 true;;
Hello, world.
- : unit = ()
# f2 false;;
- : unit = ()

または括弧の代わりにbegin ~ endをつかう。こっちのほうが「よいスタイル」だと推奨されているらしい。

# let f3 b = if b then begin print_string "Hello, "; print_string "world.\n" end;;
val f3 : bool -> unit = <fun>
# f3 true;;
Hello, world.
- : unit = ()
# f3 false;;
- : unit = ()

繰り返し

while は

while [式1] do [式2] done

という形をしていて,式1が真であるあいだ式2を繰り返す。while を使った fact の例:

# let fact n =
let i = ref 1 and res = ref 1 in
while (!i <= n) do
res := !res * !i; i := !i + 1
done;
!res
;;
val fact : int -> int = <fun>
# fact 5;;
- : int = 120

for は

for [変数] = [式1] to [式2] do [式3] done

または

for [変数] = [式1] downto [式2] do [式3] done

という形をしていて,[変数]を整数[式1]から[式2]まで順に束縛しながら[式3]を評価する。for を使って fact を定義してみよう。

# let fact2 n =
let res = ref 1 in
for i = 1 to n do
res := !res * i
done;
!res
;;
val fact2 : int -> int = <fun>
# fact2 5;;
- : int = 120

多相性と書き換え可能データ

let で名前の付けられる式が値でない場合,多相性に制約がつくことがある。値でないとはたとえば参照などだ。

# let x = ref [];;
val x : '_a list ref = {contents = []}

x は参照で,中身は空のリストだ。空だから何のリストでもいい(多相)はずで,型も ‘_a となっている。ここで1をコンスしてみる。

# 1 :: !x;;
- : int list = [1]

当然うまくいく。x 自身を書き換えたわけではないので,まだ中身はカラリストのままだ。じゃ,今度は ‘a’ をコンスしてみると:

# 'a' :: !x;;
Characters 7-9:
'a' :: !x;;
^^
This expression has type int list but is here used with type char list

エラーになった。int のリストじゃないといけないといってる。あわてて x の型を確認してみると:

# x;;
- : int list ref = {contents = []}

int list への参照に変わってしまっている。

どういう訳なのか理解できないのだけど,はじめ多相的だったものが,いったん int list として評価されたことで型が確定してしまった,ということだろうか。少なくとも現象としてはそういうことらしい。

ちなみに,参照ではなくただの空リストなら問題ない。

# let y = [];;
val y : 'a list = []
# 1 :: y;;
- : int list = [1]
# 'a' :: y;;
- : char list = ['a']

配列

配列はリストと似ているけど

  • 長さが生成時に固定される
  • 各要素に直接アクセスできる
  • 書き換え可能

という点で違う。各要素は同じ型でないといけないのはリストと同じ。

生成:

# let ary = [| 1; 2; 3; 4; 5 |];;
val ary : int array = [|1; 2; 3; 4; 5|]
# ary;;
- : int array = [|1; 2; 3; 4; 5|]

要素の参照:

# ary.(3);;
- : int = 4

リストと違って先頭からたどる必要がない。

書き換え:

# ary.(3) <- 333;;
- : unit = ()
# ary;;
- : int array = [|1; 2; 3; 333; 5|]

書き換え可能なデータ構造:文字列

実は,文字列は書き換えが可能。たとえば次のような文字列があったとして:

# let s = "life";;
val s : string = "life"
# s;;
- : string = "life"

次のように書き換えができる。

# s.[2] <- 'k';;
- : unit = ()
# s;;
- : string = "like"

新しい値ができるのではなくて書き換わっている。

構造的等価性と物理的等価性

2つのデータを比べたとき,「値として等しいこと」を構造的等価性(structural equality)という。「値として」だけでなく,メモリ上の同じ位置を占めていることを物理的等価性(physical equality)という。

OCaml には構造的等価性を調べる演算子 = と物理的等価性を調べる演算子 == がある。

たとえば:

# "life" = "life";;
- : bool = true
# "life" == "life";;
- : bool = false

2つの “life” は値としては等しい(構造的等価性)けど,メモリ上の同じ位置は占めていないので,== による比較(物理的等価性)では false になっている。

一方,いったん変数を束縛してやった場合には:

# s = "life";;
- : bool = false
# s = s;;
- : bool = true
# s == s;;
- : bool = true

両方とも true になる。

ふーむ,このあたりは Ruby と同じだと思っておけばいいか。

書き換え可能なレコード

レコードを宣言するときにフィールド名の前に mutable キーワードをつけることで,書き換え可能にすることができる。

# type teacher = {name : string; mutable office : string};;
type teacher = { name : string; mutable office : string; }

これで office フィールドを書き換えることができるレコードができた。

具体的な値を作って:

# let t = {name = "Igarash"; office = "140"};;
val t : teacher = {name = "Igarash"; office = "140"}

書き換えてみよう。書き換えは文字列の場合と似ていて,. (ドット)の後にフィールド名を書く。

# t.office <- "142";;
- : unit = ()
# t;;
- : teacher = {name = "Igarash"; office = "142"}

ちゃんと書き換わった。

参照

書き換え可能なレコードの特殊な場合で,フィールド1つだけを持つ場合を伝統的に参照(reference)という。参照には特別な書き方がある。

まず,参照の生成には ref 関数。

# let p = ref 4;;
val p : int ref = {contents = 4}

ref は初期値を引数にとって,参照を返す。ref は多相的な関数なので引数の型は何でもいい。

# let s = ref "foo";;
val s : string ref = {contents = "foo"}

int ref とか string ref が参照の型。

参照の値を取り出すには前置演算子 ! を使う。

# !p;;
- : int = 4
# !s;;
- : string = "foo"

参照の書き換えは代入(assignment)と呼ぶことが多い。:= 演算子を使う。

# p := 7;;
- : unit = ()
# p;;
- : int ref = {contents = 7}

参照は型が決まっているので,違う型の値は代入できない。

# s := 77;;
Characters 5-7:
s := 77;;
^^
This expression has type int but is here used with type string

exn型,exception宣言

Not_found とか Division_by_zero とかいう例外は,じつは exn型のコンストラクタ。例外コンストラクタと呼ぶ。

コンストラクタの型を見ると

# Division_by_zero;;
- : exn = Division_by_zero

exn型であることがわかる。同様に raise の型も。

# raise;;
- : exn -> 'a = <fun>

exn型には後からコンストラクタを追加することができる。これが新しい例外を宣言することに相当し,exception宣言を使う。

# exception Other_exception;;
exception Other_exception
# Other_exception;;
- : exn = Other_exception

引数をとる例外の場合は of に続けて引数の型をかけばいい。

# exception Another_exception of string;;
exception Another_exception of string
# Another_exception "some error";;
- : exn = Another_exception "some error"

練習問題 7.2

整数リストの要素すべての積を返す関数 prod_list を定義しなさい。リスト要素の一つでも 0 が含まれている場合には,prod_list の適用結果は常に 0 になるので,例外処理を使用 して,0 を発見したら残りの計算を行わずに中断して 0 を返すように定義しなさい。

まずは 0 をかけるという例外を宣言しよう。

# exception Multiple_zero;;
exception Multiple_zero

でもって prod_list の定義。

# let prod_list lis =
let prod' x y = if y = 0 then raise Multiple_zero else x * y in
try List.fold_left prod' 1 lis with
Multiple_zero -> 0
;;
val prod_list : int list -> int = <fun>

これでいいはず。

# prod_list [1; 3; 7; 2; 9];;
- : int = 378
# prod_list [5; 2; 0; 8; 4];;
- : int = 0