M.Hiroi's Home Page
http://www.geocities.jp/m_hiroi/

Functional Programming

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

[ PrevPage | OCaml | NextPage ]

例外

「例外 (exception) 」は主にエラー処理で使われる機能です。「例外=エラー処理」と考えてもらってもかまいません。Common Lisp には「コンディション (condition) 」という例外処理があります。最近は例外処理を持っているプログラミング言語が多くなりました。もちろん、OCaml にも例外処理があります。

●例外の定義

OCaml にはあらかじめ定義されている例外があります。たとえば、次の例を見てください。

# 4 / 0;;
Exception: Division_by_zero.
# List.tl (List.tl [1]));;
Exception: Failure "tl".

最初の例は 0 で除算した場合です。例外処理を何も行っていなければ、OCaml は処理を中断して Exception: と表示します。そして、その後ろに例外の種類を表示します。この場合は Division_by_zero という例外が発生したことがわかります。次の例は空リストに tl を適用した場合です。この場合は Failure という例外が tl で発生したことがわかります。

なお、例外が発生したことを、「例外が送出された」という場合もあります。このドキュメントでは、「例外を送出する」とか「例外が送出された」と記述することにします。

例外は exception を使って、ユーザが独自に定義することができます。

exception 名前
exception 名前 of 型式

たとえば、exception Foo とすると例外 Foo が定義されます。OCaml の場合、例外の名前を英大文字から始めます。例外に引数を渡す場合は型式を指定します。たとえば、exception Bar of int * int とすると、例外 Bar に整数を 2 つ渡すことができます。それでは、実際に定義してみましょう。

# exception Foo;;
exception Foo
# Foo;;
- : exn = Foo
# exception Bar of int * int;;
exception Bar of int * int
# Bar (1, 2);;
- : exn = Bar (1, 2)

OCaml の場合、例外は exn というヴァリアント型で表されていて、例外の名前は exn 型のコンストラクタになります。ヴァリアントの場合、定義したあとでコンストラクタを追加することはできませんが、exn 型に限ってコンストラクタを追加することができるようになっています。

●例外の送出

例外を送出するには raise を使います。

raise 例外

たとえば、raise Foo とすれば例外 Foo が送出されます。raise (Bar (1, 2)) とすれば、例外 Bar が送出されます。次の例を見てください。

# let foo n = if n < 0 then raise Foo else 1;;
val foo : int -> int = <fun>
# foo 1;;
- : int = 1
# foo (-1);;
Exception: Foo.

# let bar a b = if a < 0 || b < 0 then raise (Bar (a, b)) else 1;;
val bar : int -> int -> int = <fun>
# bar 1 1;;
- : int = 1
# bar (-1) 1;;
Exception: Bar (-1, 1)

関数 foo n は、n < 0 の場合に例外 Foo を送出し、それ以外の場合は 1 を返します。関数 bar a b は、a < 0 または b < 0 の場合に例外 Bar を送出し、それ以外の場合は 1 を返します。if E then F else G は式 F と G の返り値が同じデータ型でなければいけませんが、式 F が例外を送出する raise なので、式 G のデータ型は何でもかまいません。

それでは簡単な例題として、階乗を求める関数 fact に引数をチェックする処理を追加してみましょう。次のリストを見てください。

リスト 1 : 階乗

exception Negative

let fact n =
  let facti n a =
    if n = 0 then a
    else facti (n - 1) (n * a)
  in
    if n < 0 then raise Negative
    else facti n 1

最初に exception で例外 Nagative を定義します。そして、局所関数 facti を呼び出す前に引数 n の値をチェックします。n < 0 であれば raise で例外 Nagative を送出します。簡単な実行例を示します。

# fact 10;;
- : int = 3628800
# fact (-10);;
Exception: Negative.

今回は例外 Negative を定義してそれを送出しましたが、OCaml に定義されている例外 Invalid_argument を送出してもよいでしょう。

●例外の捕捉

OCaml の場合、送出された例外を「捕捉 (catch) 」することで、処理を中断せずに継続することができます。処理をやり直したい場合や特別なエラー処理を行いたい場合、例外処理はとても役に立ちます。OCaml では、次の式で例外を捕捉することができます。

try 式0 with
  パターン1 -> 式1
| パターン2 -> 式2
  ...
| パターンn -> 式n

例外を捕捉するには try 式を使います。式 0 の処理で例外が送出されると、その例外とマッチングするパターンの節を選択し、対応する式を評価します。そして、その結果が try 式の返り値となります。式 0 が正常に終了した場合は、その評価結果が try 式の返り値になります。

簡単な例を示しましょう。次のリストを見てください。

リスト 2 : 例外の捕捉

exception Foo of int

let foo n = if n < 0 then raise (Foo n) else 1  

let foo1 n =
  try foo n with
    Foo (-1) -> print_string "Foo error 1\n"; 0
  | Foo (-2) -> print_string "Foo error 2\n"; 0
  | Foo _ -> print_string "Foo error 3\n" ; 0

関数 foo は引数 n が負の場合に例外 Foo を送出します。関数 foo1 は foo を呼び出し、例外が送出された場合は try で捕捉します。例外の引数はパターンマッチングで取り出すことができます。Foo (-1) の場合は "Foo error 1" を表示して 0 を返します。関数 foo の返り値は int なので、例外を捕捉したときに評価する式の返り値も int でなければいけません。ご注意ください。

Foo (-2) の場合は "Foo error 2" を表示して 0 を返します。最後のパターンは匿名変数が使われているので、その他の数値はこの規則とマッチングします。"Foo error 3" を表示して 0 を返します。

それでは、実行してみましょう。

# foo (-1);;
Exception: Foo (-1).
# foo1 2;;
- : int = 1
# foo1 (-1);;
Foo error 1
- : int 0
# foo1 (-2);;
Foo error 2
- : int = 0
# foo1 (-4);;
Foo error 3
- : int = 0

foo (-1) をそのまま評価すると例外を送出します。foo1 (-1) を評価すると foo が送出した例外を捕捉して、エラー処理を行ってから 0 を返しています。foo1 (-2) も foo1 ( -4) も例外 Foo を捕捉しています。このように、例外を捕捉して処理を続行することができます。


書き換え可能なデータ型

ML (SML/NJ, OCaml など) は関数型言語なので、関数の引数や let で定義した局所変数の値を書き換えることはできません。ところが、ML には値を書き換えることができるデータ型が用意されています。OCaml の場合、「配列 (array) 」と「参照 (reference) 」というデータ型は値を書き換えることができます。また、レコードの場合もフィールド名の前にキーワード mutable を付けると書き換え可能なデータ型になります。

これらのデータ型を使うと、手続き型言語のように「副作用」を伴う操作を行うことができます。関数型言語としては不純な機能なのですが、問題によっては配列や参照を使った方が簡単にプログラムできる場合もあります。

●配列

OCaml の配列はC言語の 1 次元配列のことで、型は 'a array で表されます。配列は入れ子にできるので、多次元配列も簡単に実現することができます。なお、OCaml の配列は、リストと同様に要素は同じデータ型でなければいけません。

OCaml の場合、配列は [| a1; a2; ...; an |] で生成します。配列のアクセスは次の式で行います。

参照 : 式.(添字)
更新 : 式.(添字) <- 式

C言語と同様に、配列の要素は 0 から数えます。簡単な例を示しましょう。

# let a = [|10; 20; 30; 40; 50|];;
val a : int array = [|10; 20; 30; 40; 50|]
# a.(0);;
- : int = 10
# a.(4);;
- : int = 50
# a.(0) <- 100;;
- : unit = ()
# a;;
- : int array = [|100; 20; 30; 40; 50|]

# let aa = [| [|10; 20; 30|]; [|40; 50; 60|] |];;
val aa : int array array = [| [|10; 20; 30|]; [|40; 50; 60|] |]
# aa.(0).(0);;
- : int = 10
# aa.(1).(2);;
- : int = 60
# aa.(0).(0) <- 100;;
- : unit = ()
# aa;;
- : int array array = [| [|100; 20; 30|]; [|40; 50; 60|] |]

配列 a の大きさは 5 なので、添字の範囲は 0 から 4 になります。範囲外の添字を指定するとエラーになります。a.(0) はC言語の a[0] と同じで、a.(0) <- 100 は a[0] = 100 と同じです。値を書き換えたときの返り値は unit になります。

配列の中に配列を入れると 2 次元配列になります。この場合、要素のアクセスは aa.(添字1).(添字2) になります。たとえば、aa.(0).(0) は最初の aa.(0) で 0 番目の要素を指定します。これは配列 [|10; 20; 30|] ですね。次の .(0) でその 0 番目の要素

を指定するので、値は 10 になります。値を書き換える場合も同じです。

●配列の操作関数

ここで、モジュール Array に用意されている関数をいくつか紹介しましょう。配列は関数 Array.make と Array.init で生成することができます。

# Array.make;;
- : int -> 'a -> 'a array = <fun>
# Array.init;;
- : int -> (int -> 'a) -> 'a array = <fun>

Array,make は第 1 引数に配列の大きさを指定し、第 2 引数の値で配列を初期化します。Array.init は第 1 引数に配列の大きさを指定するのは同じですが、第 2 引数には要素の初期値を与える関数を渡します。この関数には配列の添字が引数として渡されます。簡単な例を示しましょう。

# Array.make 5 0;;
- : int array = [|0; 0; 0; 0; 0|]
# Array.init 5 (fun x -> x);;
- : int array = [|0; 1; 2; 3; 4|]

配列の大きさは関数 Array.length で求めることができます。

# let a = Array.make 5 0;;
val a : int array = [|0; 0; 0; 0; 0;|]
# Array.length a;;
- : int = 5

関数 Array.append は 2 つの配列を結合した新しい配列を返します。

# Array.append [|1; 2; 3|] [|4; 5; 6|]
- : int array = [|1; 2; 3; 4; 5; 6|]

関数 Array.to_list は配列をリストに、Array.of_list はリストを配列に変換します。

# Array.to_list [|0; 1; 2; 3 ;4|]
- : int list = [0; 1; 2; 3; 4]
# Array.of_list [0; 1; 2; 3; 4]
- : int array = [|0; 1; 2; 3; 4|]

Array には配列用の高階関数も用意されています。

val Array.map : ('a -> 'b) -> 'a array -> 'b array
val Array.iter : ('a -> unit) -> 'a array -> unit
val Array.fold_left : ('a -> 'b -> 'a) -> 'a -> 'b array -> 'a
val Array.fold_right : ('a -> 'b -> 'b) -> 'a array -> 'b -> 'b

map, iter, fold_left, fold_right は今までに説明した高階関数の配列バージョンです。このほかにも便利な関数が用意されています。詳細は OCaml のリファレンスをお読みください。

●参照

「参照 (reference) 」はデータを間接的に参照する機能です。一般に、変数束縛は変数とデータを直接結び付けます。これに対し、参照は変数とデータを直接結び付けるのではなく、その間にデータを指し示す特別なデータ型を入れます。このデータ型を「参照型」といいます。次の図を見てください。

                  データ
┌───┐      ┌───┐
│変数 a│──→│  11  │
└───┘ 束縛 └───┘

  (A) 通常の束縛

                   参照型           データ
┌───┐      ┌──┬─┐      ┌───┐
│変数 b│──→│ref │・┼──→│  11  │  
└───┘ 束縛 └──┴─┘      └───┘

  (B) 参照型データを束縛

        図 1 : 参照 (1)

上図 (A) は通常の変数束縛です。let a = 11 とすると、変数 a は数値 11 に束縛されます。これに対し、図 (B) が参照を図示したものです。変数 b は参照型データに束縛され、参照型データが数値 11 を指し示しています。変数 b は参照型データを経由して数値 11 を参照することができます。

OCaml で参照型データを生成するには ref を使います。次の例を見てください。

# let b = ref 11;;
val b : int ref = {contents = 11}
# let c = ref "foo";;
val c : string ref = {contents = "foo"}

ref 11 は数値 11 を指し示す参照型データを生成し、それを変数 b に束縛します。この場合、データの型は int ref になります。ref "foo" は文字列 "foo" を指し示す string ref を生成し、それを変数 c に束縛します。

参照先のデータを求めるには演算子 ! を使います。変数 b と c に演算子 ! を適用すると、次のようになります。

# !b;;
- : int = 11
# !c;;
- : string = "foo"

参照型データは参照するデータを変更することができます。次の図を見てください。

                   参照型           データ
┌───┐      ┌──┬─┐      ┌───┐
│変数 b│──→│ref │・┼ X →│  11  │
└───┘ 束縛 └──┴┼┘      └───┘
                        │        ┌───┐
                        └───→│  22  │
                                  └───┘
        (C) データの書き換え

            図 2 : 参照 (2)

上図 (C) は参照するデータを数値 11 から数値 22 に変更しています。すると、変数 b が参照する値は 22 になります。ようするに、変数 b の値を書き換えることと同じ効果が得られるわけです。値の書き換えは演算子 := で行います。次の例を見てください。

# b := 22;;
- : unit = ()
# !b;;
- : int = 22
# c := "bar";;
- : unit = ()
# !c;;
- : string = "bar"

b := 22 とすると、変数 b の値は 11 から 22 に書き換えられます。同様に、c := "bar" とすると、変数 c の値は "foo" から "bar" になります。

●書き換え可能なレコード

OCaml のレコードはフィールド名にキーワード mutable を付けると、その値を書き換えることができます。実をいうと、OCaml はレコードを使って ref を実現しています。

type 'a ref = {mutable contents : 'a}

値の書き換えは次のように行います。

式0.フィールド名 <- 式1

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

# type foo = {mutable bar : int; baz = float};;
type foo = {mutable bar : int; baz = float}
# let a = {bar = 10; baz = 1.23};;
val a : foo = {bar = 10; baz = 1.23}
# a.bar <- 100;;
- : unit = ()
# a;;
val a : foo = {bar = 100; baz = 1.23}

●多相性の制限

書き換え可能なデータ型を使う場合、多相性が制限されることがあります。たとえば、空リスト [ ] の参照を考えてみましょう。実際に ref [ ] で参照を生成すると次のようになります。

# let a = ref [];;
val a = '_a list ref = {contents = []}
# a := [1; 2; 3];;
- : unit = ()
# a;;
- : int list ref = {contents = [1; 2; 3]}

生成されるデータ型は '_a list になります。'_a は型変数ではなく、OCaml が型を推論できずに暫定的に付けた型名で、'a list のような多相的な型ではありません。たとえば、a に [1; 2; 3] をセットすると、データ型は int list ref になり、ここでデータ型が決定されます。このあと、他のデータ型に変更することはできません。もしも、'a list のような多相的な型の参照を許すと、次のような代入操作が可能になります。

let a = ref []       (* 型が 'a list とする *)

a := [1; 2; 3]       (* int list に書き換える *)
a := ["abc"; "def"]  (* string list に書き換える *)

変数 a の型が int list から string list に変わってしまいます。つまり、プログラムの実行中に変数の型が変化する可能性があるわけです。OCaml はコンパイル時にデータ型を厳密にチェックするプログラミング言語です。実行中にデータ型が変化するような処理は、OCaml には想定外のことです。参照を使う場合は多相性を制限する必要があるのです。

実際にはもう少し制限が厳しくて、変数に「値」を束縛するときに限り、多相性を許すようになっています。参考文献 [1] によると、これを「値多相」と呼ぶそうです。この場合の「値」とは、変数、定数、関数定義、要素が値の組やレコードなどです。逆に、関数や ref の適用、mutable を含むレコード、if など評価が行われる式は「値」になりません。次の例を見てください。

# let times2 f x = f (f x);;
val times2 : ('a -> 'a) -> 'a -> 'a = <fun>
# times2 (fun x -> x * 2) 10;;
- : int = 40
# let times4 = times2 times2;;
val times4 : ('_a -> '_a) -> '_a -> '_a = <fun>
# times4 (fun x -> x * 2) 10;;
- : int = 160
# times4;;
val times4 : (int -> int) -> int -> int = <fun>

関数 f を 2 回適用する関数 times2 を定義します。times2 は多相型関数になります。次に、times2 を使って関数を 4 回適用する関数 times4 を定義します。ここで部分適用を使って let times4 = times2 times2 と定義します。すると、times4 は多相型関数にはなりません。let の右辺は関数の定義ではなく、部分適用によって生成された関数が与えられるため、多相性が制限されるのです。次のように関数として定義すれば、times4 は多相型関数になります。

# let times4' f x = times2 times2 f x;;
val times4' : ('a -> 'a) -> 'a -> 'a = <fun>

多相性の話は難しいですね。参考文献 [1] には型推論と多相性のことがわかりやすく説明されています。興味のある方は一読されることをおすすめします。

●繰り返し

今まで繰り返しは再帰定義を使ってきましたが、OCaml には簡単な繰り返しを行う while 式と for 式が用意されています。まずは while 式から説明しましょう。

while 式1 do 式2 done

while 式は 式 1 を評価し、その結果が真である限り式 2 を繰り返し評価します。while 式の処理を図に示すと次のようになります。

              ↓
              ├←─────┐ 
      偽┌─────┐      │
┌───│条件:式 1 │      │
│      └─────┘      │
│            ↓真          │
│      ┌─────┐      │
│      │   式 2   │      │
│      └─────┘      │
│            └──────┘
└──────┐
              ↓

        図 3 : while の処理

図を見ればおわかりのように while 式はいたって単純ですが、条件が偽にならないと「無限ループ」に陥ってしまうので注意してください。while 式はユニットを返します。簡単な例を示しましょう。

リスト 3 : 階乗 (while 版)

let fact n =
  let i = ref n and v = ref 1 in
  while !i > 0 do
    v := !v * !i;
    i := !i - 1
  done;
  !v

階乗を求める関数 fact を while 式を使ってプログラムしています。let で変数 i, v を定義し、その値を書き換えながら階乗 n * (n - 1) * ... * 2 * 1 を計算しています。最後に階乗の値 !v を返します。

for 式は次の 2 通りの形式があります。

(1) for 変数 = 式1 to 式2 do 式3 done
(2) for 変数 = 式1 downto 式2 do 式3 done

式 1 と式 2 の評価結果は整数 (int) でなければいけません。式 1, 2 の値が m, n とすると、(1) は m, m + 1, m + 2, ..., n の順に変数を束縛して式 3 を繰り返し評価します。(2) は m, m - 1, m - 2, ..., n の順に変数を束縛して式 3 を繰り返し評価します。

簡単な例として、階乗のプログラムを for 式で書き直してみましょう。

リスト 4 : 階乗 (for 版)

let fact n =
  let v = ref 1 in
  for i = 2 to n do
    v := !v * i
  done;
  !v

while 式を使うよりも簡単になりましたね。while 式や for 式は配列と組み合わせて使うと便利です。

●パスカルの三角形

もう一つ簡単な例題として、再帰定義 の「組み合わせの数」で作成した関数 comb を使って「パスカルの三角形」を作ってみましょう。次の図を見てください。

                1                                 0C0
              /  \                              /  \
            1      1                         1C0    1C1
          /  \  /  \                      /  \  /  \
        1      2      1                 2C0    2C1    2C2
      /  \  /  \  /  \              /  \  /  \  /  \
    1      3      3      1         3C0    3C1    3C2    3C3
  /  \  /  \  /  \  /  \      /  \  /  \  /  \  /  \
1      4      6      4      1 4C0    4C1    4C2    4C3    4C4 

                       図 4 : パスカルの三角形

パスカルの三角形は、左側の図のように両側がすべて 1 で、内側の数はその左上と右上の和になっています。これは (a + b)n を展開したときの各項の係数を表しています。そして、その値は右側の図のように組み合わせの数 nr に対応しています。

きれいな三角形にはなりませんが、パスカルの三角形を表示するプログラムを示します。

リスト 5 : パスカルの三角形を表示

let pascal x =
  for n = 0 to x - 1 do
    for r = 0 to n do
      print_int (comb (n, r));
      print_string " "
    done;
    print_newline ()
  done

for 式で「二重ループ」を構成しています。最初のループで n の値を 0 から x - 1 まで増やし、次のループで r の値を 0 から n まで増やしています。あとは comb (n, r) を計算して値を表示するだけです。

それでは実行結果を示します。

# pascal 10;;
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1
1 8 28 56 70 56 28 8 1
1 9 36 84 126 126 84 36 9 1
val it = () : unit

ところで、関数 comb を使わなくてもパスカルの三角形を出力するプログラムを作ることができます。リストと配列のどちらを使っても簡単です。

パスカルの三角形は、両側がすべて 1 で内側の数はその左上と右上の和になっています。したがって、n 段の三角形を作る場合、一段前の値を記憶しておいて隣同士の要素を足し算すればいいわけです。一段前の値をリストに格納すると、プログラムは次のようになります。

リスト 6 : パスカルの三角形 (リスト版)

(* int list の表示 *)
let print_intlist xs =
  List.iter (fun x -> print_int x; print_string " ") xs;
  print_newline ()

(* パスカルの三角形*)
let rec pascal_list n a =
  let rec pascal_sub = function
      _ :: [] -> [1]
    | x1 :: x2 :: xs -> (x1 + x2) :: pascal_sub (x2 :: xs)
    | _ -> raise (Failure "pascal_sub")
  in
  print_intlist a;
  if n > 1 then pascal_list (n - 1) (pascal_sub (0 :: a))

局所関数 pascal_sub は、リストの隣同士の要素を足した値をリストに格納して返します。この処理は再帰呼び出しを使えば簡単です。リストの先頭要素と次の要素を足し算し、その値を再帰呼び出しの返り値にコンス演算子で追加すればいいわけです。

リストの要素がひとつになると、最初の節とマッチングするので再帰呼び出しが停止します。ここでリスト [1] を返します。また、pascal_sub を呼び出すときはリストの先頭に 0 を追加します。これで、リストの先頭と最後尾を 1 にすることができます。あとは、関数 pascal_list で pascal_sub を呼び出すだけです。実行結果は同じなので省略します。

次は、配列を使ってプログラムを作ります。n 段の三角形を作る場合、大きさが n + 1 の配列を用意します。たとえば、6 段の三角形を作ってみましょう。次の図を見てください。

     0  1  2  3  4  5  6
  ------------------------- 
1 [  0  1  0  0  0  0  0  ]
      \|\|
2 [  0  1  1  0  0  0  0  ]
      \|\|\|
3 [  0  1  2  1  0  0  0  ]
      \|\|\|\|
4 [  0  1  3  3  1  0  0  ]
      \|\|\|\|\|
5 [  0  1  4  6  4  1  0  ]
      \|\|\|\|\|\|
6 [  0  1  5 10 10  5  1  ]

図 5 : パスカルの三角形の求め方

最初に配列の内容を 0 に初期化し、1 番目に 1 をセットします。これが 1 段目になります。あとは図のように、隣の要素を足し算するだけです。プログラムは次のようになります。

リスト 7 : パスカルの三角形 (配列版)

(* int array の表示 *)
let print_intarray n ary =
  for i = 1 to n do
    print_int ary.(i); print_string " "
  done;
  print_newline ()

(* パスカルの三角形 *)
let pascal_array n =
  let ary = Array.make (n + 1) 0 in
  ary.(1) < 1;
  print_intarray 1 ary;
  for i = 1 to n - 1 do
    for j = i + 1 downto 1 do
      ary.(j) <- ary.(j) + ary.(j - 1);
    done;
    print_intarray (i + 1) ary
  done

配列はひとつあれば十分です。ただし、配列の値を書き換えていくので、配列の後方から計算していくことに注意してください。前方から計算すると、値がおかしくなります。実行結果は同じなので省略します。

リストと配列を使ってパスカルの三角形を作りましたが、もっとクールな方法があるかもしれません。興味のある方は挑戦してみてください。


Copyright (C) 2008 Makoto Hiroi
All rights reserved.

[ PrevPage | OCaml | NextPage ]