今回は Erlang の「例外 (exception) 処理」について取り上げます。一般に、例外はエラー処理で使われる機能です。「例外=エラー処理」と考えてもらってもかまいません。最近は例外処理を持っているプログラミング言語が多くなりました。もちろん Erlang にも例外処理があり、throw で例外を送出して catch で例外を捕捉することができます。
ただし、並行プログラミングの場合、catch と throw による例外処理だけでは十分とはいえません。Erlang の場合、あるプロセスで例外が送出されたとき、それを他のプロセスへ通知して、後始末をする仕組み (link と monitor) が用意されています。今回は Erlang の基本的な例外処理について説明します。
Erlang では、送出された例外を捕まえるのに try を使います。try の構文を下図に示します。
try 式 [, 式a, ..., 式z] of pattern1 [ガード節] -> body1; ・・・・・ patternN [ガード節] -> bodyN catch class1:pattern1 [:Stack] [ガード節] -> exception_body1; ・・・・・ classN:patternN [:Stack] [ガード節] -> exception_bodyN after after_body end 図 : 例外処理
try の基本的な動作は case と似ています。try は式の評価結果 (式が複数ある場合は最後の式の評価結果) と pattern をマッチングし、成功した節を選択して右辺の body を評価します。式の実行中に例外が送出された場合、式の実行は中断され、catch で指定したパターンと例外をマッチングします。そして、マッチングに成功した節を選択して、右辺の exception_body を評価します。after で指定した式 after_body は、例外の有無にかかわらず最後に必ず実行されます。
Erlang の例外は 3 つのクラス [*1] に分類されます。
エラーの重度は throw < exit < error の順になります。また、以前のバージョンでは関数 erlang:get_stacktrace/0 を使ってスタックトレースを取得しましたが、Erlang/OTP 21 からは catch 節のパターンの後ろに変数を指定することで、スタックトレースを取得することができるようになりました。
例外は関数 throw/1, exit/1, error/1 で送出することができます。error は重大なエラーが発生したことを通知するために使います。exit はプロセスを終了したい場合に使います。引数にアトム normal を渡して呼び出すと正常終了、それ以外の値を渡すと異常終了したことになります。その他の例外は throw を使います。
簡単な例を示しましょう。次のリストを見てください。
リスト : 例外処理 -module(exception). -export([catcher/1]). catcher(F) -> try F() of V -> {normal, V} catch throw:X -> {throw, X}; exit:X -> {exit, X}; error:X -> {error, X} end.
関数 catcher/1 は高階関数で、引数 F を評価してその返り値をタプルに格納して返します。例外が送出された場合、それを catch で捕捉して例外の種類とその値をタプルに格納して返します。
それでは実行してみましょう。
> exception:catcher(fun() -> foo end). {normal,foo} > exception:catcher(fun() -> throw(foo) end). {throw,foo} > exception:catcher(fun() -> exit(foo) end). {exit,foo} > exception:catcher(fun() -> error(foo) end). {error,foo} > a + 1. ** exception error: bad argument in an arithmetic expression in operator +/2 called as a + 1 > exception:catcher(fun() -> a + 1 end). {error,badarith}
最初の例は foo を返すだけなので、例外は送出されません。返り値は {normal, foo} になります。次の例は throw(foo) を実行するので、例外が送出されます。それが catch で捕捉されるので、返り値は {throw, foo} になります。exit も error も同様です。最後の例は、a + 1 でランタイムエラーになります。ランタイムエラーも catch で捕捉することができます。
catch の節で、例外のクラスを変数にすることもできます。たとえば、catcher/1 を書き直すと次のようになります。
リスト : 例外処理 (2) catcher1(F) -> try F() of V -> {normal, V} catch E:X -> {E, X} end.
この場合、例外であれば何でもマッチングすることになります。
> exception:catcher1(fun() -> foo end). {normal,foo} > exception:catcher1(fun() -> throw(foo) end). {throw,foo} > exception:catcher1(fun() -> exit(foo) end). {exit,foo} > exception:catcher1(fun() -> error(foo) end). {error,foo} > exception:catcher1(fun() -> a + 1 end). {error,badarith}
ところで、例外の捕捉は catch だけでも行うことができます。
catch 式 => 値
catch は式を評価した値を返しますが、例外を捕捉した場合は、そのクラスによって以下に示す値を返します。
error と exit の場合はタプル {'EXIT', ...} を返し、throw の場合はその引数を返します。簡単な例を示しましょう。
> catch error(oops). {'EXIT',{oops,[{shell,apply_fun,3, [{file,"shell.erl"},{line,907}]}, {erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,684}]}, {erl_eval,expr,5,[{file,"erl_eval.erl"},{line,437}]}, {shell,exprs,7,[{file,"shell.erl"},{line,686}]}, {shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]}, {shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]}} > catch exit(oops). {'EXIT',oops} > catch throw(oops). oops > catch (fun() -> oops end)(). oops > catch (fun() -> throw(oops) end)(). oops
catch だけで例外を捕捉できるのは確かに便利なのですが、最後の例のように、関数の返り値なのか例外 (throw の値) なのか区別できなくなる場合もありえます。使用するときには十分に注意してください。
それから、try の of は省略することができます。その場合は次の式と同じ動作になります。
try 式 catch ... end == try 式 of X -> X catch ... end
つまり、エラーがなければ式の評価結果をそのまま返すことになります。簡単な実行例を示します。
> try 1 + 2 catch E:R -> {E, R} end. 3 > try a + 2 catch E:R -> {E, R} end. {error,badarith}
例外処理を使うと、評価中の関数からほかの関数へ制御を移す「大域脱出 (global exti)」を実現することができます。
簡単な例を示しましょう。
リスト : 大域脱出 bar1() -> io:format('call bar1~n'). bar2() -> throw(global_exit). bar3() -> io:format('call bar3~n'). foo() -> bar1(), bar2(), bar3(). baz() -> try foo() of V -> V catch throw:X -> X end.
> exception:baz(). call bar1 global_exit
実行の様子を下図に示します。
┌──────┐ │try ..... │←──┐ └──────┘ │ ↓ │ ┌──────┐ │ │ foo() │──┐│ └──────┘ ││ ↓↑ ↓│ ┌──────┐ ┌ bar2() ──┐ │ bar1() │ │ throw(...) │ └──────┘ └──────┘ 図 : 大域脱出
通常の関数呼び出しでは、呼び出し元の関数に制御が戻ります。ところが bar2/0 で throw が実行されると、呼び出し元の関数 foo/0 を飛び越えて、制御が try の catch に移るのです。このように、例外処理を使って関数を飛び越えて制御を移すことができます。
例外処理を使った大域脱出はとても強力な機能ですが、多用すると処理の流れがわからなくなる、いわゆる「スパゲッティプログラム」になってしまいます。使用には十分ご注意下さい。
ところで、プログラムの途中で例外が送出されると、残りのプログラムは実行されません。このため、必要な処理が行われない場合があります。このような場合、try で after を指定すると解決できます。after で指定された式は try で例外が発生したかどうかにかかわらず、try の実行が終了するときに必ず実行されます。
簡単な例を示しましょう。大域脱出で作成した foo/0 を呼び出す関数 baz1/0 を作ります。
リスト : after 節 baz1() -> try foo() of V -> V catch throw:X -> io:format('catch ~w~n', [X]), X after io:format('clean_up~n') end.
baz1/0 は after を指定しているので、例外の有無にかかわらず clean_up と必ず表示されます。実行例を示します。
> exception:baz1(). call bar1 catch global_exit clean_up global_exit
throw で例外が送出され、baz1/0 の catch で捕捉されます。このとき、右辺の式が評価されるので catch global_exit が表示されます。それから after で指定した式が評価されるので clean_up と表示されます。baz1/0 の返り値は after で実行された式の結果ではなく、try または catch で選択された節で実行された式の値になります。この場合は X の値 global_exit になります。