「例外 (exception)」は主にエラー処理で使われる機能です。「例外=エラー処理」と考えてもらってもかまいません。Common Lisp には「コンディション (condition)」という例外処理があります。最近は例外処理を持っているプログラミング言語が多くなりました。もちろん、SML/NJ にも例外処理があります。
SML/NJ にはあらかじめ定義されている例外があります。たとえば、次の例を見てください。
- 4 div 0; uncaught exception Div [divide by zero] raised at: stdIn:10.3-10.6 - tl(tl [1]); uncaught exception Empty raised at: smlnj/init/pervasive.sml:195.19-195.24 -
最初の例は 0 で除算した場合です。例外処理を何も行っていなければ、SML/NJ は処理を中断して uncaught exception と表示します。そして、その後ろに例外の種類を表示します。この場合は divide by zero という例外が発生したことがわかります。次の例は空リスト (nil) に tl を適用した場合です。この場合は Empty という例外が発生します。
なお、例外 Empty が発生したことを、「例外 Empty が送出された」という場合もあります。このドキュメントでは、「例外を送出する」とか「例外が送出された」と記述することにします。
例外は exception を使って、ユーザが独自に定義することができます。
exception 名前 exception 名前 of 型式
たとえば、exception Foo とすると例外 Foo が定義されます。SML/NJ では、例外の名前を英大文字から始める習慣があります。例外に引数を渡す場合は型式を指定します。たとえば、exception Bar of int * int とすると、例外 Bar に整数を 2 つ渡すことができます。それでは、実際に定義してみましょう。
- exception Foo; exception Foo - Foo; val it = Foo(-) : exn - exception Bar of int * int; exception Bar of int * int - Bar; val it = fn : int * int -> exn
SML/NJ の場合、例外は exn というデータ型になります。型式を指定すると、Bar は例外を返す関数として定義されます。そして、関数 Bar の返り値は例外 Bar になります。
例外を送出するには raise を使います。
raise 例外
たとえば、raise Foo とすれば例外 Foo が送出されます。raise Bar( 1, 2 ) とすれば、例外 Bar が送出されます。次の例を見てください。
- fun foo n = if n < 0 then raise Foo else 1; val foo = fn : int -> int - foo 1; val it = 1 : int - foo ~1; uncaught exception Foo - fun bar(a, b) = if a < 0 orelse b < 0 then raise Bar(a, b) else 1; val bar = fn : int * int -> int - bar(1, 1); val it = 1 : int - bar(1, ~1); uncaught exception Bar
関数 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 の評価結果が if の値になります。
それでは簡単な例題として、階乗を求める関数 fact に引数をチェックする処理を追加してみましょう。次のリストを見てください。
リスト : 階乗 exception Negative fun fact n = let fun facti(0, a) = a | facti(n, a) = facti(n - 1, n * a) in if n < 0 then raise Negative else facti(n, 1) end
最初に exception で例外 Nagative を定義します。そして、関数 facti を呼び出す前に引数 n の値をチェックします。n < 0 であれば raise で例外 Nagative を送出します。簡単な実行例を示します。
- fact 10; val it = 3628800 : int - fact ~10; uncaught exception Negative
SML/NJ の場合、送出された例外を「捕捉 (catch)」することで、処理を中断せずに継続することができます。処理をやり直したい場合や特別なエラー処理を行いたい場合、例外を捕捉できると便利です。SML/NJ では、次の式で例外を捕捉することができます。
expr handle pat1 => expr1 | pat2 => expr2 | ... | patN => exprN
例外を捕捉するには handle を使います。式 expr の処理で例外が送出されると、その例外とマッチングするパターンの規則を選択し、対応する式を評価します。そして、その結果が handle の返り値となります。式 expr が正常に終了した場合は、expr の評価結果が handle の返り値になります。
簡単な例を示しましょう。次のリストを見てください。
リスト : 例外の捕捉 exception Foo of int fun foo n = if n < 0 then raise Foo n else 1 fun foo1 n = foo n handle Foo ~1 => (print "Foo error 1\n"; 0) | Foo ~2 => (print "Foo error 2\n"; 0) | Foo _ => (print "Foo error 3\n"; 0)
関数 foo は引数 n が負の場合に例外 Foo を送出します。関数 foo1 は foo を呼び出し、例外が送出された場合は handle で捕捉します。例外の引数はパターンマッチングで取り出すことができます。Foo ~1 の場合は "Foo error 1" を表示して 0 を返します。関数 foo の返り値は int なので、例外を捕捉したときに評価する式の返り値も int でなければいけません。ご注意ください。
Foo ~2 の場合は "Foo error 2" を表示して 0 を返します。最後のパターンは匿名変数が使われているので、その他の数値はこの規則とマッチングします。"Foo error 3" を表示して 0 を返します。
それでは、実行してみましょう。
- foo ~1; uncaught exception Foo - foo1 2; val it = 1 : int - foo1 ~1; Foo error 1 val it = 0 : int - foo1 ~2; Foo error 2 val it = 0 : int - foo1 ~4; Foo error 3 val it = 0 : int
foo ~1 をそのまま評価すると例外を送出します。foo1 ~1 を評価すると foo が送出した例外を捕捉して、エラー処理を行ってから 0 を返しています。foo1 ~2 も foo1 ~4 も例外 Foo を捕捉しています。このように、例外を捕捉して処理を続行することができます。