9. エラーと例外

9.1. そんなに速くない!

../_images/cyclist1.png

この章以外に適切な場所がありませんでした。 これまでいろんなエラーを見せてきましたが、エラーハンドリング機構に関してはまだ十分説明してませんでした。 (あるいはエラー自体の紹介も不十分でした) Erlangには2つのパラダイムがあります。関数型と並列性です。 関数型から連想されるものは巻頭から紹介してきました。参照透過性、再帰、高階関数などです。 並列性から連想されるものがErlangを有名にしたのです。アクター、何千もの並列プロセス、監視ツリーなどです。

関数型に関する部分が並列性に関する部分に進むより前にやるべき本質的なことだと思うので、この章では関数型に関連する部分しか紹介しません。 エラーの対処をするときはまずこれらを理解しなければいけません。

Note

Erlangには関数型のコード内のエラーを処理するのにいくつかの方法がありますが、たいていの場合はクラッシュさせたほうがいいでしょう。 このヒントは イントロダクション で紹介しました。 プログラムをこのように扱う機構に関しては並列性に関する部分で説明します。

9.2. エラー大集合

たくさんのエラーがあります。コンパイル時エラー、論理エラー、ランタイムエラー、生成エラーです。 この節ではコンパイル時エラーに注目し、その他のエラーに関しては次の節で説明します。

コンパイル時エラーはしばしば構文の間違いによって引き起こされます。関数名、言語のトークン(波括弧、丸括弧、ピリオド、カンマ)、アリティ、などを確認しましょう。 ここによくあるコンパイル時エラーメッセージの一覧とそれらの解決方法を載せます:

module.beam: Module name ‘madule’ does not match file name ‘module’
-module 属性内に書いたモジュール名がファイル名と一致していません
./module.erl:2: Warning: function some_function/0 is unused
関数を公開していない、あるいはその関数が使われている場所が間違った関数名やアリティになっています。 このエラーはもう必要ない関数を書いた場合にも起きえましょう。コードを確認すること!
./module.erl:2: function some_function/1 undefined
関数が存在していません。 -export 属性内あるいは関数を宣言するときに間違った関数名やアリティを書いています。 このエラーは関数がコンパイルされてない場合にも起きえます。 これは通常関数のあとにピリオドを書いてないなどの構文エラーによります。
./module.erl:5: syntax error before: ‘SomeCharacterOrWord’
これは様々な理由から起こりえます。 たとえば、括弧の閉じ忘れやタプルやおかしな式接尾辞( case の最後の節をカンマで終えないなど)があります。 他の理由としては、予約語を使ったりおかしな文字コードにエンコードされたUnicode文字を使ったりしているなどがあります。(実際に見たことがあります!)
./module.erl:5: syntax error before:
そう、このエラーはあまり説明的ではありません。 このエラーは通常行末がおかしな時に起きます。 これは前のエラーの特定の場合なので、よく注意してみてください。
./module.erl:5: Warning: this expression will fail with a ‘badarith’ exception
Erlangは常に動的型付けをしていますが、型は強力だということを忘れないでください。 この場合、コンパイラは十分賢く、算術式が失敗すると分かっています。(たとえば llama + 5 ) けれど、これ以上複雑な型エラーは見つけてくれません。
./module.erl:5: Warning: variable ‘Var’ is unused
宣言以後使わない変数を宣言しています。 これはコードにおいてバグになるので、書いたものは再確認しましょう。 あるいは、変数名を _ に援交するかコードの可読性を上げるために変数名の接頭辞としてアンダースコアを付けましょう。
./module.erl:5: Warning: a term is constructed, but never used
あなたが書いた関数の中には、リストを作ったり、タプルを宣言したり、どんな変数にも束縛されていない無名関数や、それ自身を返すような無名関数を宣言したりするでしょう。 この警告は何か必要ないことをしていたり、間違っていることを知らせます。
./module.erl:5: head mismatch
関数の先頭が複数宣言できて、それぞれのアリティが異なっていても構いません。 異なるアリティになると異なる関数になることを忘れないでください。 そして、関数宣言では別のアリティを持つ関数の宣言を挿し込むことはできません。 同様に、このエラーは関数定義を他の関数での先頭の節の間に差し込むと起きます。
./module.erl:5: Warning: this clause cannot match because a previous clause at line 4 always matches
モジュール内で定義された関数がcatch-all節のあとに特定の節を持っています。 そのようなときはコンパイラは他の節に行く必要がないことを警告してくれます。
./module.erl:9: variable ‘A’ unsafe in ‘case’ (line 5)
case ... of の中で宣言されている変数を、その外側で使っている場合に起きます。 これは安全でないと解釈されます。そのような変数を使いたい場合は MyVar = case ... of ... を使うべきです。

いま上に挙げたエラーは現段階で起こり得るコンパイル時エラーをほとんど網羅していると思います。 数はそれほど多くないですし、たいていの場合は、一番難しいのは多くのエラーが続いている中で、どのエラーがそれを引き起こしているのかを見つけるところです。 コンパイラエラーは表示された順に解決していくことをおすすめします。そうしないと、実際には全然関係ないエラー取り組むはめになります。 上に挙げていないエラーに遭遇することもありますし、そういったエラーに遭遇した場合は私にメールを送ってください。 できるだけ早く上のリストに追加します。

9.3. 「あなたの」ロジックが間違ってる!

../_images/exam1.png

論理エラーは最も見つけにくく、デバッグしにくいエラーの一つです。 論理エラーはたいていの場合プログラマによって引き起こされます。 if節やcase節がすべての条件を考慮していなかったり、掛け算と割り算を間違えていたりするなどです。 こういった間違いはプログラムをクラッシュさせることはないですが、眼に見えない使えないデータを作ったり、予期しない動作をさせる原因になります。

こういったエラーが起きた場合、たいていの場合は自力で解決しなければいけないのですが、Erlangではテストフレームワーク、TypErやDialyzer(型の章で説明しました)、デバッガやトレースモジュールなど、多くの機能がそれをサポートします。 コードをテストすることがこういったエラーを回避する最も良い方法です。 悲しいことに、各プログラマのキャリアごとにこういったエラーは本を何十冊とかけるほど出てくるので、これ以上エラーについて語ることはしません。 プログラムをクラッシュさせるエラーに注目するほうが簡単です。 なぜならエラーはすぐそこで起こっていて、今から50レベルも上がるわけではないからです。 この考え方がすでに何度も言っている「クラッシュするならさせておけ」というアイデアの基になっています。

9.4. ランタイムエラー

ランタイムエラーはコードをクラッシュさせるという点でとても有害です。 Erlangはクラッシュに対応するのが得意ですが、エラーの原因を認識すれば常に役立ちます。 このようなことから、よくあるランタイムエラーの小さなリストを、エラーの説明とそのエラーを生成するサンプルコード一緒に作りました。

関数節 (function_clause)
1> lists:sort([3,2,1]).
[1,2,3]
2> lists:sort(fffffff).
** exception error: no function clause matching lists:sort(fffffff)

関数内のすべてのガード節で失敗する、あるいはすべてのパターンマッチで失敗するというのが、このエラーが起きる上で最も多い原因です。

case節 (case_clause)
3> case "Unexpected Value" of
3>    expected_value -> ok;
3>    other_expected_value -> 'also ok'
3> end.
** exception error: no case clause matching "Unexpected Value"

特定の条件を書くのを忘れたか、間違った種類のデータを送ったか、catch-all節が必要かのどれかです!

if節 (if_clause)
4> if 2 > 4 -> ok;
4>    0 > 1 -> ok
4> end.
** exception error: no true branch found when evaluating an if expression

これは case節 の時ととてもよく似ています。 true と評価される節が見つからないときに、このエラーが起きます。 すべての条件を考えるか、catch-all節を追加して、 true があるようにしましょう。

間違ったマッチ (badmatch)
5> [X,Y] = {4,5}.
** exception error: no match of right hand side value {4,5}

badmatchエラーはパターンマッチが失敗したときに起きます。 これはたいてい無理なパターンマッチ(たとえば上のコードにあるもの)をしようとしていたり、変数に2回束縛をしようとしていたり、 = 演算子の左右で等しくないものを置いていたりしている場合(これは再束縛を失敗させている原因です!)に起きます。 このエラーは、プログラマが _MyVar の形式の変数は _ と同じだと思っている時に起きます。 アンダースコアがついた変数は、コンパイラがその変数が使われていない場合に警告をださないだけで、扱いは普通の変数です。 この変数に1回より多くの束縛を行うことはできません。

間違った引数 (badarg)
6> erlang:binary_to_list("heh, already a list").
** exception error: bad argument
     in function  binary_to_list/1
        called as binary_to_list("heh, already a list")

これは、関数を間違った引数で呼び出しているという点で 関数節 にとてもよく似ています。 大きな違いは、このエラーはガード節の外側で、関数の引数を検証している時に起きていて、通常プログラマによって引き起こされます。 この章のあとのほうでどのようにこのようなエラーを投げるかお見せします。

未定義 (undef)
7> lists:random([1,2,3]).
** exception error: undefined function lists:random/1

これは存在しない関数を呼び出したときに起きます。 (もしモジュールの外側で呼び出しているなら)関数がモジュールから正しいアリティで公開されていることを確認して下さい。 そして、関数名かモジュール名をtypoしてないか再度確認してください。 他の理由としては、モジュールがErlangの検索パスにないことが考えられます。 デフォルトでは、Erlangの検索パスはカレントディレクトリに設定されています。 パスは code:add_patha/1 または code:add_pathz/1 を使って追加できます。 もしこれでもうまくいかなかったら、モジュールがちゃんとコンパイルされているか確認して下さい!

間違った算術演算 (badarith)
8> 5 + llama.
** exception error: bad argument in an arithmetic expression
     in operator  +/2
        called as 5 + llama

このエラーは存在しない算術演算をしようとしたときに起きます。 たとえばゼロ割りやアトムと数字による割り算です。

間違った関数 (badfun)
9> hhfuns:add(one,two).
** exception error: bad function one
in function  hhfuns:add/2

このエラーの最も多い原因は、関数ではない変数を関数として使ってしまうことです。 上の例では、 前の章 で書いた hhfuns 関数に2つのアトムを関数として渡しました。 これがうまく動作しなかったのでこのエラーが投げられたのです。

間違ったアリティ (badarity)
10> F = fun(_) -> ok end.
#Fun<erl_eval.6.13229925>
11> F(a,b).
** exception error: interpreted function with arity 1 called with two arguments

間違ったアリティのエラーは間違った関数の特別な場合場合です。 高階関数を使っていて、必要な数の引数よりも多いあるいは少ない引数を渡したときに起きます。

システム限界
システム限界のエラーが投げられるにはたくさんの理由があります。 プロセスが多すぎる、アトムが長すぎる、関数に渡した引数が多すぎる、アトムの数が多すぎる、ノードの接続数が多すぎる、などです。 よくある原因が、 Erlang Efficiency GuideErlang Efficiency Guid日本語訳 )の「システムの限界」の章に紹介されているので読んでみてください。 いくつかは深刻なもので、VM全体をクラッシュさせる可能性があります。

9.5. 例外を上げる

../_images/stop1.png

コードの実行結果を監視して、論理エラーから守るために、しばしば問題点が早く見つかるようにランタイムにクラッシュさせるのが良いことがあります。

Erlangには3種類の例外があります。エラー(error)、スロー(throw)、終了(exit)です。 これらはすべて異なった用途で用いられます:

9.5.1. エラー (error)

erlang:error(Reason) を呼び出すと、現在のプロセス内で実行されていることが終わって、エラーをキャッチしたときに、最後に呼び出した関数を呼び出した引数含めてスタックトレースを取得します。 これが上に挙げたランタイムエラーを引き起こす例外です。

エラーは関数がいま起きたことを制御できないときに、行っていることを止めるための手段です。 if節 エラーが出た場合は何が出来るでしょうか? コードを変更して再コンパイルする。(よくできたエラーメッセージを表示する以外には)それができることです。 エラーを使うべきでない例は再帰の章で使った二分木モジュールでしょう。 あのモジュールは参照するときに常に木の中から特定のキーを見つけられるというわけではありません。 このような場合、ユーザに不明という結果を処理させるようにする方がいいでしょう。 デフォルト値を使って、木に新しい値を挿入して、木を削除して、などなどといった具合です。 このような場合はエラーを上げるより {ok, Value} のような形のタプルを返すか、あるいは none というアトムを返すのが適切でしょう。

さて、エラーは上に挙げたような例に限定されません。あなた独自のエラーも定義できます:

1> erlang:error(badarith).
** exception error: bad argument in an arithmetic expression
2> erlang:error(custom_error).
** exception error: custom_error

ここで custom_error はErlangシェルに解釈されず、”bad argument in ...”のようなカスタムした変換メッセージなどもありません。 しかし、badargと同様に使えて、プログラマから分かりやすい形で扱うことが出来ます。(どうやって使うかすぐに紹介します)

9.5.2. 終了 (exit)

終了には2種類あります。 「内部」終了と「外部」終了です。 内部終了は exit/1 関数を呼び出すことで発生し、今走っているプロセスの動作を止めます。 外部終了は exit/2 関数を呼び出すことで発生し、Erlangの並列な面で起こるマルチプロセスと関係があります。 このような理由から、内部終了に重きを置いて、外部終了については後ほど触れます。

内部終了はエラーととても良く似ています。実際、歴史的に言えば、両方共一緒のもので、 exit/1 が存在していただけでした。 ユースケースにおいては両方はあまり同じではありません。どちらから選んだらいいんでしょうか? それははっきりとした答えはありません。どちらをどういうときに使うべきかを理解するには、一からアクターとプロセスの概念を理解するほかありません。

イントロダクションで、プロセスをメールでやりとりしている人間に例えました。そのアナロジーに追加するものはほとんどありません。こんどはダイアグラムと水玉で見てみましょう。

../_images/a-b-msg1.png

ここでプロセスはお互いにメッセージを送りあっています。これで、残りの説明ができます。 プロセスはメッセージをリッスンすることも、待つことも出来ます。どのメッセージをリッスンするかを選ぶこともできますし、いくつか捨てることもできますし、他を無視したり、一定時間経過後にリスニングをやめることも出来ます。

../_images/a-b-c-hello1.png

これらの基本的な概念によって、Erlangの実装者はプロセス間で例外のやりとりをする特別なメッセージを使うことができます。 これらのメッセージはプロセスの死に際の台詞のようなもので、プロセスが死ぬ直前に投げられ、コードは停止します。 特定の種類のメッセージをリッスンしている他のプロセスは、そのイベントを知ることが出来、その後すべきことが出来ます。 これはロギングや死んだプロセスの再起動などを含みます。

../_images/a-b-dead1.png

このコンセプトの基に、 erlang:error/1exit/1 の使い分けは簡単に出来ます。 ともに非常に似たような使い方ができますが、本当の違いはその意図にあります。 「単純に」得たものがエラーなのか、今のプロセスを殺すべき状態なのかを選ぶことが出来ます。 この点は erlang:error/1 がスタックトレースを返して、 exit/1 が返さないことからも明白です。 大きなスタックトレースを取得しようとしていたり、現在の関数にとても多くの引数を渡したりしたい場合は、すべてのリスニングプロセスに終了メッセージをコピーするということはデータをコピーするという事になります。 いくつかの場合においてはこれは非実用的でしょう。

9.5.3. スロー (throw)

スローは例外のクラスで、プログラマが予想した自体に対処するために使われます。 終了やエラーと比較して、「プロセスをクラッシュしろ!」とは伝えず、フローを制御しようとします。 スローはプログラマがそれに対処するように投げているわけですが、そのスローを含むモジュールに使い方を書いておくのはいいことです。

例外を投げるための構文は次の形です:

1> throw(permission_denied).
** exception throw: permission_denied

permission_denied の部分は好きなものに替えられます。( 'everything is fine!' に替えることもできますが、なんの役にも立たないし、友達もなくすでしょう)

スローは深い再帰を使っているときには局所的でない返り値としても使うことが出来ます。 例は ssl モジュール内で見ることが出来ます。 そこでは throw/1{error, Reason} のタプルを最上位の関数に渡す手段として使われています。 実装する人は、正常系だけ書いて、あとは1つ例外処理関数をトップレベルに書いておけばよくなります。

他の例はarrayモジュールで見ることが出来ます。ここではもし見つけたい要素がなかった場合は、ユーザが指定した default 値を返すという参照関数があります 要素が見つからなかった場合は、デフォルト値は例外として投げられ、最上位の関数はその値を処理してユーザが指定したデフォルト値に置き換えます。 これはモジュールの作者が参照アルゴリズムを持ったすべての関数にユーザ指定のデフォルト値を渡さなくてよくなり、再度言いますが、正常系にのみ集中することが出来ます。

../_images/catch1.png

大まかなやり方としては、モジュール内でスローを使うときはデバッグしやすいように、局所的でない返り値として使うことだけに限定しましょう。 またこうすることでモジュールのインターフェースを変更することなくモジュール内部を変更しやすくなります。

9.5.4. 例外を処理する

これまで何度もスロー、エラー、終了は処理できると書いてきました。 それは try ... catch 式を使うことで出来ます。

try ... catch は式を評価して、正常系とエラーを同時に扱うことが出来ます。 一般的な構文は次のようになります:

try Expression of
    SuccessfulPattern1 [Guards] ->
        Expression1;
    SuccessfulPattern2 [Guards] ->
        Expression2
catch
    TypeOfError:ExceptionPattern1 ->
        Expression3;
    TypeOfError:ExceptionPattern2 ->
        Expression4
    end.

tryof の間の式は保護されていると言われています。 これは、そこに書かれている式から発生したどんな種類の例外でもキャッチされるということです。 try ... ofcatch の間に書かれたパターンと式は case ... of と全く同様に動作します。 最後は catch 部です。ここでは、TypeOfErrorを error, throw, exit のどれかに変更出来ます。 これら3つについてはすでに見てきました。もしタイプが指定されなかったら、 throw だと判断されます。 では実際に使ってみましょう。

まず始めに、 exceptions モジュールから始めて見ましょう。 簡単にこんな形で始めて見ます:

-module(exceptions).
-compile(export_all).

throws(F) ->
    try F() of
        _ -> ok
    catch
        Throw -> {throw, caught, Throw}
    end.

コンパイルして、違った種類の例外も試してみます:

1> c(exceptions).
{ok,exceptions}
2> exceptions:throws(fun() -> throw(thrown) end).
{throw,caught,thrown}
3> exceptions:throws(fun() -> erlang:error(pang) end).
** exception error: pang

見ての通り、ここでの try ... catch はスローだけを受け取っています。 前に言ったとおり、これはタイプが指定されていなかったら、スローだと判断されるからです。 それではそれぞれのタイプのcatch節を持った関数を書いてみます:

errors(F) ->
    try F() of
        _ -> ok
    catch
        error:Error -> {error, caught, Error}
    end.

exits(F) ->
    try F() of
        _ -> ok
    catch
        exit:Exit -> {exit, caught, Exit}
    end.

これを試してみます:

4> c(exceptions).
{ok,exceptions}
5> exceptions:errors(fun() -> erlang:error("Die!") end).
{error,caught,"Die!"}
6> exceptions:exits(fun() -> exit(goodbye) end).
{exit,caught,goodbye}

次はどのようにすべてのタイプの例外をまとめて1つの try ... catch にするかを示したメニューの例です。 最初にすべての例外を生成する関数を宣言します:

sword(1) -> throw(slice);
sword(2) -> erlang:error(cut_arm);
sword(3) -> exit(cut_leg);
sword(4) -> throw(punch);
sword(5) -> exit(cross_bridge).

black_knight(Attack) when is_function(Attack, 0) ->
    try Attack() of
        _ -> "None shall pass."
    catch
       throw:slice -> "It is but a scratch.";
       error:cut_arm -> "I've had worse.";
       exit:cut_leg -> "Come on you pansy!";
       _:_ -> "Just a flesh wound."
    end.

ここで is_function/2 は変数Attackがアリティ0の関数かを確認するBIFです。 そしてこの1行を尺度として書いておきます:

talk() -> "blah blah".

今度は全く違うことをしてみます

7> c(exceptions).
{ok,exceptions}
8> exceptions:talk().
"blah blah"
9> exceptions:black_knight(fun exceptions:talk/0).
"None shall pass."
10> exceptions:black_knight(fun() -> exceptions:sword(1) end).
"It is but a scratch."
11> exceptions:black_knight(fun() -> exceptions:sword(2) end).
"I've had worse."
12> exceptions:black_knight(fun() -> exceptions:sword(3) end).
"Come on you pansy!"
13> exceptions:black_knight(fun() -> exceptions:sword(4) end).
"Just a flesh wound."
14> exceptions:black_knight(fun() -> exceptions:sword(5) end).
"Just a flesh wound."
../_images/black-knight1.png

9行目の式は black_knight の通常の動作を示しています。 その後の行はそれぞれ例外クラス(throw, error, exit)とそれに付随した理由( slice, cut_arm, cut_leg )に対応したパターンマッチの示しています。

式13と14で示されているのは、例外に対するcatch-all節の扱い方です。 _:_ パターンはどんなタイプのどんな例外も確実にキャッチする時に使います。 実際にcatch-allパターンを使うときは注意すべきです。コードを例外処理できるものから保護して、それ以上はしないようにしてください。 Erlangには残りを扱うための他のツールがあります。

さらに try ... catch に続いて追加できる節があります。 これは常に実行され、他の言語で言うところの ‘finally’ ブロックと同等の働きをします:

try Expr of
    Pattern -> Expr1
catch
    Type:Exception -> Expr2
after % this always gets executed
    Expr3
end

エラーが起きたかに関係なく、 after の内部の式は実行されることが保証されています。 しかしながら、 after 節からは返り値は取得できません。 したがって、 after ではたいてい副作用があるコードを書くことになります。 この節の典型的な使い方は、例外が上げられたどうかに限らず、確実に読み込んでいるファイルを閉じたい、などがあります。

Erlangのcatchブロックで使える例外の3クラスの扱い方はわかりました。 しかしながら、まだ隠していることがあります。実際には tryof の間には2つ以上の式を書くことができるのです!

whoa() ->
    try
        talk(),
        _Knight = "None shall Pass!",
        _Doubles = [N*2 || N <- lists:seq(1,100)],
        throw(up),
        _WillReturnThis = tequila
    of
        tequila -> "hey this worked!"
    catch
        Exception:Reason -> {caught, Exception, Reason}
    end.

exception:whoa() を呼び出すことで、 throw(up) によって {caught, throw, up} を取得します。 やりました、これで tryof の間に2つ以上の式を持つことができます。

exception:whoa/0 は、たくさんの式をこういう形で使うと、私たちは常に返り値が何かを気にしているわけではないということに気づいていないことを明らかにしました。 of 部はいささか意味のないものになっています。 しかし良いニュースです。次のように of を使わないこともできます:

im_impressed() ->
    try
        talk(),
        _Knight = "None shall Pass!",
        _Doubles = [N*2 || N <- lists:seq(1,100)],
        throw(up),
        _WillReturnThis = tequila
    catch
        Exception:Reason -> {caught, Exception, Reason}
    end.

これでちょっとすっきりしました!

Note

例外から保護された部分が末尾再帰になりえないことを知っておくのは重要です。 VMは常に例外が起きた場合に備えて参照を張っておかなければなりません。

of 部がない try ... catch は保護部分しかないので、再帰関数をそこで呼ぶのは、プログラムが長い時間動作する(Erlangが得意なこと)上では危険です。 イテレーションをしすぎると、メモリ不足になったり、理由もわからず動作が遅くなったりします。 ofcatch の間に再帰呼び出しをすれば、保護部分ではないので、末尾呼び出し最適化の恩恵を得ることが出来ます。

明らかに再帰でないコードで結果がどうでもいいような場合を除いて、そのような予期しないエラーを避けるためにデフォルトでは try ... catch よりも try ... of ... catch を使う人もいます。 何をしようとたいていは自分でルールを決めて構いません!

9.6. 待って、まだあるよ!

まだまだ他の言語と比べると十分ではないようなので、Erlangにはまだ他のエラー処理機構があります。 その機構はキーワード catch として定義されていて、基本的にあらゆる種類の例外をを良い結果の上で取得します。 例外と違った表現をするので、ちょっと奇妙に見えます:

1> catch throw(whoa).
whoa
2> catch exit(die).
{'EXIT',die}
3> catch 1/0.
{'EXIT',{badarith,[{erlang,'/',[1,0]},
{erl_eval,do_apply,5},
{erl_eval,expr,5},
{shell,exprs,6},
{shell,eval_exprs,6},
{shell,eval_loop,3}]}}
4> catch 2+2.
4

ここでわかるのは、スローは一緒ですが、終了とエラーはともに {'EXIT', Reason} と表されていることです。 これはエラーが終了のあとに言語に追加された結果です。(後方互換のために似た表現のままでいます)

スタックトレースの読み方は次のとおりです:

5> catch doesnt:exist(a,4).
{'EXIT',{undef,[{doesnt,exist,[a,4]},
{erl_eval,do_apply,5},
{erl_eval,expr,5},
{shell,exprs,6},
{shell,eval_exprs,6},
{shell,eval_loop,3}]}}
  • エラーのタイプは undef で、呼び出した関数が定義されてないということを意味しています(この章の最初に書いたリストを見てください)
  • エラーのタイプに続くリストはスタックトレースです
  • スタックトレースの最初のタプルは最後に呼ばれた関数を表しています。( {Module, Function, Arguments} ) これはエラーを起こした未定義の関数です。
  • それに続くタプルはエラーが起きる前に呼ばれた関数です。今度は {Module, Function, Arity の形になっています。
  • ここにあるのは本当にこれだけです。

erlang::get_stacktrace/0 を呼び出すことで、プロセスがクラッシュしたときにスタックトレースを手動で取得することが出来ます。

次のように catch が使われていることがしばしばあります。( exceptions.erl に書かれています):

catcher(X,Y) ->
    case catch X/Y of
        {'EXIT', {badarith,_}} -> "uh oh";
        N -> N
    end.

予想通りに次のようになります:

6> c(exceptions).
{ok,exceptions}
7> exceptions:catcher(3,3).
1.0
8> exceptions:catcher(6,3).
2.0
9> exceptions:catcher(6,0).
"uh oh"

例外をキャッチをするのがコンパクトで簡単のように思えます。しかし catch にはいくつか問題があります。 最初の問題は演算子の優先権です:

10> X = catch 4+2.
* 1: syntax error before: 'catch'
10> X = (catch 4+2).
6

たいていの式はこのように括弧でくくる必要はないので、これはあまり直感的ではありません。 catch の他の問題は、ここで出した例外と本当の例外との違いを見分けるのが難しいことです:

11> catch erlang:boat().
{'EXIT',{undef,[{erlang,boat,[]},
{erl_eval,do_apply,5},
{erl_eval,expr,5},
{shell,exprs,6},
{shell,eval_exprs,6},
{shell,eval_loop,3}]}}
12> catch exit({undef, [{erlang,boat,[]}, {erl_eval,do_apply,5}, {erl_eval,expr,5}, {shell,exprs,6}, {shell,eval_exprs,6}, {shell,eval_loop,3}]}).
{'EXIT',{undef,[{erlang,boat,[]},
{erl_eval,do_apply,5},
{erl_eval,expr,5},
{shell,exprs,6},
{shell,eval_exprs,6},
{shell,eval_loop,3}]}}

これではエラーと実際の終了との違いを知ることができません。 上記の例外を生成するのに throw/1 を使うことも出来ました。 実際 catch の中の throw/1 は別のシナリオで問題になるでしょう。

one_or_two(1) -> return;
one_or_two(2) -> throw(return).

次は致命的な問題です:

13> c(exceptions).
{ok,exceptions}
14> catch exceptions:one_or_two(1).
return
15> catch exceptions:one_or_two(2).
return

catch の後ろにいるので、関数が例外を投げたのかreturnが実際の値なのか絶対にわかりません! これら全ては実践ではほんとに起きるわけではないかもしれませんが、それでも try ... catch 節に加えてR10Bのリリースでこれが追加されたのには十分な理由があります:

9.7. 二分木でtryをトライする

例外を実践で使うために、私たちの tree モジュールをさらに進めてちょっとした演習をしてみましょう。 二分木を検索して木の中に指定した値がすでに存在するか判定する関数を追加してみます。 二分木はキーによって並び替えされていて、かつ今回の演習ではキーについては考える必要がないので、値を見つけるまで木を走査する必要があります。

木を操作するのは大枠では tree:lookup/2 でしたことと同様です。 ただし今回は常に左側の枝を検索したらい右側の枝を検索します。 この関数を書くには、木のノードは {node, {Key, Value, NodeLeft, NodeRight}} か空の時は {node, nil} を持っていることさえ思い出せば良いです。 この前提で、例外なしの基本的な実装ができます:

has_value(_, {node, 'nil'}) ->
    false;
has_value(Val, {node, {_, Val, _, _}}) ->
    true;
has_value(Val, {node, {_, _, Left, Right}}) ->
    case has_value(Val, Left) of
        true -> true;
        false -> has_value(Val, Right)
    end.

この実装で問題になるのは、分岐した木のすべてのノードが、前の枝の結果をテストしなければいけないという点です。

../_images/tree-case1.png

これはちょっとイライラします。スローを使うことで、ちょっと比較を減らすことができます:

has_value(Val, Tree) ->
    try has_value1(Val, Tree) of
        false -> false
    catch
        true -> true
    end.

has_value1(_, {node, 'nil'}) ->
    false;
has_value1(Val, {node, {_, Val, _, _}}) ->
    throw(true);
has_value1(Val, {node, {_, _, Left, Right}}) ->
    has_value1(Val, Left),
    has_value1(Val, Right).

上に書いたコードを実行すると、前のコードと同じような結果になりますが、この場合は返り値を確認する必要がなくなります。 全く気にしなくてよくなります。 このバージョンでは、スローは単に値が見つかったことを意味するだけです。 スローされたら、二分木の評価は止まって、最上位にある catch に戻ります。 そうでなければ、最後のfalseが返されるまで評価が続いて、それがユーザへの返り値となります:

../_images/tree-throw1.png

もちろん、この実装は前の実装よりも長くなっています。 しかし、この実装はより速く、よりすっきりしたものになっています。 これはスローによる局所的でない返り値を使ったことと、行っている操作に依存します。 今の例は単純な比較で、もうこれ以上進めるものはありませんが、実践でより複雑なデータ構造や操作を扱うときには意味があるものとなります。

これで、逐次式のErlangで実際の問題を解く準備ができたでしょう。