M.Hiroi's Home Page

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

例外


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

はじめに

「例外 (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 rec 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 で例外 Negative を定義します。そして、局所関数 facti を呼び出す前に引数 n の値をチェックします。n < 0 であれば raise で例外 Negative を送出します。簡単な実行例を示します。

# 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 を捕捉しています。このように、例外を捕捉して処理を続行することができます。

●問題

中置記法で書かれた数式を計算するプログラムを作ってください。数式はリストで表すことにします。リストの要素は次のように定義します。

type item = Add | Sub | Mul | Div | Rpa | Lpa | N of float

演算子は Add (+.), Sub (-.), Mul (*.), Div (/.) で、数値は実数 (float) だけとします。数式はカッコを使うことできます。右カッコを Rpa で、左カッコを Lpa で表します。

exception Expr_err
val expression : item list -> int = <fun>
# expression [N 1.; Add; N 2.; Add; N 3.; Add; N 4.];;
- : float = 10.
# expression [N 1.; Add; N 2.; Mul; N 3.; Add; N 4.];;
- : float = 11.
# expression [Lpa; N 1.; Add; N 2.; Rpa; Mul; Lpa; N 3.; Add; N 4.; Rpa];;
- : float = 21.












●解答

参考文献『C言語による最新アルゴリズム事典』の「式の評価」によると、四則演算の数式は次の構文規則で表すことができます。

式 := 項 (+ | -) 項 (+ | -) 項 ...
項 :- 因子 (* | /) 因子 (* | /) 因子 ...
因子 := 数 | (式)

これをそのままプログラムすると、次のようになります。

リスト : 数式の計算 (中置記法)

(* 例外の定義 *)
exception Expr_err

type item = Add | Sub | Mul | Div | Rpa | Lpa | N of float

let rec factor = function
  (N x)::xs -> (x, xs)
| Lpa::xs -> let (v, ys) = expr xs in
               if List.hd ys = Rpa then (v, List.tl ys)
               else raise Expr_err
| _ -> raise Expr_err
and term xs =
  let rec term_sub value = function
    [] -> (value, [])
  | Mul::xs -> let (v, ys) = factor xs in term_sub (value *. v) ys
  | Div::xs -> let (v, ys) = factor xs in term_sub (value /. v) ys
  | xs -> (value, xs)
  in
    let (v, ys) = factor xs in term_sub v ys
and expr xs =
  let rec expr_sub value = function
    [] -> (value, [])
  | Add::xs -> let (v, ys) = term xs in expr_sub (value +. v) ys
  | Sub::xs -> let (v, ys) = term xs in expr_sub (value -. v) ys
  | xs -> (value, xs)
  in
    let (v, ys) = term xs in expr_sub v ys

let expression xs =
  let (v, ys) = expr xs in
    match ys with
      [] -> v
    | _ -> raise Expr_err

関数 expr は「式」を評価します。実際の処理は局所関数 expr_sub で行います。最初に関数 term を呼び出して「項」を評価します。返り値はタプルで、値は評価結果 v と残りのリスト ys です。演算子が Add (+.) または Sub (-.) の場合、term を呼び出して式 xs を評価し、返り値を v と ys にセットします。そして、value と v を加算 (または減算) して expr_sub を再帰呼び出しします。そうでなければ、評価結果 x と残りのリスト xs をタプルで返します。

関数 term も同様の処理を行います。この場合は最初に関数 factor を呼び出して「因子」を評価します。そして、演算子が Mul (*.) または Div (/.) の場合は factor を呼び出して評価を続行します。そうでなければ、評価結果 x と残りのリスト xs をタプルで返します。関数 factor は簡単で、引数の先頭要素が数値の場合はそれをそのまま返し、Lpa であれば xs を expr に渡して評価します。戻ってきたら、リスト ys の先頭要素が Rpa であることを確認します。それ以外の場合はエラーを送出します。

最後に、関数 expression から expr を呼び出します。リスト ys が空リストでなければ式に誤りがあるのでエラーを送出します。そうでなければ計算結果 v を返します。

-- 参考文献 --------
[1] 奥村晴彦,『C言語による最新アルゴリズム事典』, 技術評論社, 1991

初版 2008 年 6 月 28 日
改訂 2020 年 6 月 28 日