エラーと例外

ちょっと待った!

A green man with a huge head and tiny body on a bicycle

このような章にふさわしい場所はありません。今までに、エラーに遭遇する可能性のある程度には学びましたが、それらをどのように処理するかを知るには十分ではありません。実際、この章ではすべてのエラー処理メカニズムを確認することはできません。それは、Erlang には主に 2 つのパラダイムがあるためです。関数型と並行型です。関数型のサブセットは、私が本の最初から説明してきたものです。参照透過性、再帰、高階関数などです。並行型のサブセットは、Erlang を有名にしたものです。アクター、数千もの並行プロセス、スーパービジョンツリーなどです。

並行部分に進む前に関数型部分を知ることが不可欠だと判断したため、この章では言語の関数型サブセットのみを扱います。エラーを管理するには、まずエラーを理解する必要があります。

注: Erlang には関数型コードでエラーを処理するいくつかの方法がありますが、ほとんどの場合、クラッシュさせるだけで済むと言われます。はじめにでこれについて少し触れました。このようにプログラミングできるようにするメカニズムは、言語の並行部分にあります。

エラーの分類

エラーには、コンパイル時エラー、論理エラー、実行時エラー、および生成されたエラーの多くの種類があります。このセクションではコンパイル時エラーに焦点を当て、次のセクションでその他のエラーについて説明します。

コンパイル時エラーは、多くの場合、構文上の間違いです。関数名、言語のトークン (角括弧、丸括弧、ピリオド、カンマ)、関数のアリティなどを確認してください。以下に、一般的なコンパイル時エラーメッセージとその解決策の可能性をリストします。

module.beam: モジュール名 'madule' がファイル名 'module' と一致しません
-module 属性に入力したモジュール名がファイル名と一致しません。
./module.erl:2: 警告: 関数 some_function/0 が未使用です
関数をエクスポートしていないか、使用されている場所の名前またはアリティが間違っています。または、不要になった関数を作成した可能性もあります。コードを確認してください!
./module.erl:2: 関数 some_function/1 が未定義です
関数が存在しません。-export 属性または関数を宣言するときに、間違った名前またはアリティを記述しました。このエラーは、指定された関数がコンパイルできなかった場合にも出力されます。これは、関数をピリオドで終了するのを忘れたなどの構文エラーが原因であることがよくあります。
./module.erl:5: 構文エラー: 'SomeCharacterOrWord' の前
これは、未閉じの括弧、タプル、または間違った式終端 (case の最後の分岐をカンマで閉じるなど) など、さまざまな理由で発生します。その他の理由には、コード内で予約されたアトムを使用したり、異なるエンコーディング間で Unicode 文字が奇妙に変換されたりする可能性があります (実際に発生しました!)。
./module.erl:5: 構文エラー: の前
確かに、これはそれほど説明的ではありません!通常、これは行の終端が正しくない場合に発生します。これは前のエラーの特定のケースであるため、注意してください。
./module.erl:5: 警告: この式は 'badarith' 例外で失敗します
Erlang は動的型付けについてですが、型が強いことを覚えておいてください。この場合、コンパイラは、算術式の 1 つが失敗すること (たとえば、llama + 5) を検出するのに十分な賢さを持っています。ただし、それより複雑な型エラーは見つけることができません。
./module.erl:5: 警告: 変数 'Var' が未使用です
変数を宣言しましたが、後でそれを使用していません。これはコードのバグである可能性があるため、記述した内容を再確認してください。それ以外の場合は、変数名を _ に変更するか、コードを読みやすくするために役立つと感じる場合はアンダースコア (_Var など) をプレフィックスとして付けることをお勧めします。
./module.erl:5: 警告: 項が構築されましたが、使用されていません
関数の 1 つで、リストの構築、タプルの宣言、または匿名関数の宣言を行っているにもかかわらず、それを変数にバインドしたり返したりしていません。この警告は、無駄なことをしているか、間違いを犯したことを示しています。
./module.erl:5: ヘッダーの不一致
関数に複数のヘッダーがあり、それぞれが異なるアリティを持っている可能性があります。異なるアリティは異なる関数を意味し、そのように関数の宣言をインターリーブすることはできないことを忘れないでください。このエラーは、別の関数のヘッダー句の間に関数定義を挿入した場合にも発生します。
./module.erl:5: 警告: 行 4 の前の句が常に一致するため、この句は一致できません
モジュールで定義された関数に、キャッチオール句の後に定義された特定の句があります。そのため、コンパイラは、他の分岐に移動する必要がないことを警告できます。
./module.erl:9: 変数 'A' は 'case' (5 行目) で安全ではありません
case ... of のいずれかの分岐内で宣言された変数を、その外で使用しています。これは安全ではないと見なされます。このような変数を使用する場合は、MyVar = case ... of... を行う方が適切です。

これで、現時点でコンパイル時に発生するほとんどのエラーがカバーされるはずです。エラーはそれほど多くありませんが、ほとんどの場合、最も難しい部分は、他の関数に対してリストされたエラーの大きなカスケードを引き起こしたエラーを見つけることです。実際にはエラーではない可能性のあるエラーによって誤解されないように、報告された順序でコンパイラエラーを解決することをお勧めします。他の種類のエラーが表示されることがあり、含まれていないエラーが発生した場合は、できるだけ早くメールを送信してください。説明とともに追加します。

いいえ、あなたのロジックが間違っています!

An exam with the grade 'F'

論理エラーは、見つけてデバッグするのが最も難しい種類のエラーです。これらは、プログラマーからのエラーである可能性が最も高いです。たとえば、'if' や 'case' などの条件文の分岐で、すべてのケースを考慮していない、乗算と除算を混同しているなどです。プログラムをクラッシュさせるのではなく、未確認の不良データを出力したり、プログラムが意図しない方法で動作したりします。

この点については、ほとんどの場合自分自身で対応する必要がありますが、Erlang には、テストフレームワーク、TypEr および Dialyzer ( 型に関する章で説明) 、デバッガー、および トレースモジュールなど、多くの機能があります。コードをテストすることがおそらく最善の防御策です。残念ながら、すべてのプログラマーのキャリアには、この種のエラーが数十冊の本を書くのに十分なほど存在するため、ここではあまり時間を費やさないようにします。プログラムをクラッシュさせるエラーに焦点を当てる方が簡単です。これはその場で発生し、50 レベル後から泡立ってくることはないためです。これは、すでに何度か言及した「クラッシュさせる」という理想のほとんどの起源であることに注意してください。

実行時エラー

実行時エラーは、コードをクラッシュさせるという意味で、かなり破壊的です。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_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"
        
誰かが case の特定のパターンを忘れたか、間違った種類のデータを送信したか、キャッチオール句が必要だったようです!
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_clause エラーと非常によく似ています。true と評価される分岐を見つけることができません。すべてのケースを考慮するか、キャッチオール true 句を追加することが必要な場合があります。
badmatch
5> [X,Y] = {4,5}.
** exception error: no match of right hand side value {4,5}
        
badmatch エラーは、パターンマッチングが失敗した場合に発生します。これは、ありえないパターンマッチング (上記のような) を試みているか、変数を 2 回目にバインドしようとしているか、または = 演算子の両側で等しくないもの (これが変数の再バインドを失敗させるほとんどすべてのものです!) を試みていることを意味している可能性が最も高いです。このエラーは、プログラマーが _MyVar の形式の変数が _ と同じであると信じているために発生する場合があることに注意してください。アンダースコア付きの変数は、使用されていなくてもコンパイラが文句を言わないことを除いて、通常の変数です。それらを複数回バインドすることはできません。
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")
        
これは、関数を間違った引数で呼び出すことに関して、function_clause と非常によく似ています。ここでの主な違いは、このエラーが通常、関数内のガード句の外側から引数を検証した後、プログラマーによってトリガーされることです。この章の後半で、そのようなエラーをスローする方法を示します。
undef
7> lists:random([1,2,3]).
** exception error: undefined function lists:random/1
        
これは、存在しない関数を呼び出した場合に発生します。関数がモジュールから正しいアリティでエクスポートされていることを確認し (モジュールの外部から呼び出している場合)、関数名とモジュール名を正しく入力したことを再確認してください。メッセージが表示されるもう 1 つの理由は、モジュールが 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 つのアトムを関数として使用しています。これはうまくいかず、badfun がスローされます。
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
        
badarity エラーは、badfun の特定のケースです。高階関数を使用する際に、処理できるよりも多い (または少ない) 引数を渡した場合に発生します。
system_limit
system_limit エラーがスローされるには多くの理由があります。多すぎるプロセス (後で説明します)、長すぎるアトム、関数内の引数が多すぎる、アトムの数が多すぎる、接続されたノードが多すぎるなどです。詳細な完全なリストについては、システムの制限に関する Erlang Efficiency Guide を読んでください。これらのエラーの一部は、VM 全体をクラッシュさせるのに十分なほど深刻であることに注意してください。

例外の発生

A stop sign

コードの実行を監視し、論理エラーから保護しようとする中で、問題が早期に発見されるように実行時クラッシュを発生させることが良いアイデアである場合がよくあります。

Erlang には、エラースロー終了という 3 種類の例外があります。それらはすべて異なる用途があります (一種)

エラー

erlang:error(Reason) を呼び出すと、現在のプロセスで実行が終了し、キャッチしたときに、最後に呼び出された関数とその引数のスタックトレースが含まれます。これらは、上記の実行時エラーを引き起こす種類の例外です。

エラーは、関数が、呼び出し元のコードが直前の処理を処理できると期待できない場合に、その実行を停止するための手段です。if_clauseエラーが発生した場合、何ができるでしょうか?コードを変更して再コンパイルするしかありません(きれいなエラーメッセージを表示する以外に)。エラーを使用すべきでない例としては、再帰の章のツリーモジュールが挙げられます。このモジュールは、ルックアップ時にツリー内の特定のキーを必ず見つけられるとは限りません。この場合、ユーザーが不明な結果に対処することを期待するのは理にかなっています。ユーザーは、デフォルト値を使用したり、新しい値を挿入するかどうかを確認したり、ツリーを削除したりできます。このような場合に、エラーを発生させるのではなく、{ok, Value}の形式のタプルや、undefinedのようなアトムを返すのが適切です。

さて、エラーは上記の例に限定されません。独自のエラーを定義することもできます。

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

ここで、custom_errorはErlangシェルでは認識されず、「...での不正な引数」のようなカスタム翻訳もありませんが、同じように使用でき、プログラマーが同じ方法で処理できます(後ほど説明します)。

終了

終了には、「内部」終了と「外部」終了の2種類があります。内部終了は、関数exit/1を呼び出すことによってトリガーされ、現在のプロセスの実行を停止します。外部終了はexit/2で呼び出され、Erlangの並行処理における複数のプロセスに関係します。そのため、ここでは主に内部終了に焦点を当て、外部終了については後ほど説明します。

内部終了は、エラーと非常によく似ています。実際、歴史的に言えば、それらは同じものであり、exit/1しか存在しませんでした。それらはほぼ同じユースケースを持っています。では、どちらを選ぶべきでしょうか?その選択は簡単ではありません。どちらを使用するかを理解するには、アクターとプロセスの概念を遠くから見始めるしかありません。

はじめに、プロセスを郵便で通信する人々に例えました。この例えに追加することはあまりないので、図とバブルに進みます。

A process 'A' represented by a circle, sending a message (represented by an arrow) to a process 'B' (another circle)

ここのプロセスは、互いにメッセージを送信できます。プロセスはメッセージをリッスンしたり、メッセージを待機したりすることもできます。また、リッスンするメッセージを選択したり、一部を破棄したり、他のメッセージを無視したり、特定の時間後にリッスンをあきらめたりすることもできます。

A process 'A' sending 'hello' to a process 'B', which in turns messages C with 'A says hello!'

これらの基本概念により、Erlangの実装者は、プロセス間で例外を伝達するための特別な種類のメッセージを使用できます。それらはプロセスの最後の息吹のように機能します。プロセスが死んで、そのコードの実行が停止する直前に送信されます。特定の種類のメッセージをリッスンしていた他のプロセスは、そのイベントについて知り、それを自由に処理できます。これには、ログ記録、死んだプロセスの再起動などが含まれます。

A dead process (a bursting bubble) sending 'I'm dead' to a process 'B'

この概念を説明すると、erlang:error/1exit/1を使用する場合の違いが理解しやすくなります。どちらも非常に似た方法で使用できますが、本当の違いは意図にあります。したがって、「単に」エラーなのか、現在のプロセスを終了するに値する状況なのかを判断できます。この点は、erlang:error/1がスタックトレースを返し、exit/1が返さないという事実によって強められます。非常に大きなスタックトレースや現在の関数への多数の引数がある場合、終了メッセージをリッスンしているすべてのプロセスにコピーすることは、データをコピーすることを意味します。場合によっては、これが非現実的になる可能性があります。

スロー

スローは、プログラマーが処理することを期待できる場合に用いられる例外のクラスです。終了やエラーと比較すると、それらは「そのプロセスをクラッシュさせる!」という意図を実際には持っておらず、むしろ制御フローを目的としています。プログラマーが処理することを期待してスローを使用する場合は、それを使用しているモジュール内でその使用を文書化することをお勧めします。

例外をスローする構文は次のとおりです。

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

ここで、permission_deniedは任意の('everything is fine'を含む)ものに置き換えることができますが、それは役に立たず、友人関係を失うでしょう。

スローは、深い再帰中の非ローカルリターンにも使用できます。その例として、トップレベル関数に{error, Reason}タプルをプッシュする方法としてthrow/1を使用するsslモジュールがあります。この関数は、そのタプルをユーザーに単純に返します。これにより、実装者は成功した場合のみを記述し、すべての例外を処理する関数を最上位に1つ持つことができます。

別の例として、必要な要素が見つからない場合にユーザーが指定したデフォルト値を返すことができるルックアップ関数がある配列モジュールが挙げられます。要素が見つからない場合、値defaultが例外としてスローされ、トップレベル関数がそれを処理し、ユーザーが指定したデフォルト値に置き換えます。これにより、モジュールのプログラマーは、ルックアップアルゴリズムのすべての関数のパラメーターとしてデフォルト値を渡す必要がなくなり、成功した場合のみに集中できます。

A fish that was caught

経験則として、コードをデバッグしやすくするために、非ローカルリターンのスローの使用を単一のモジュールに制限するようにしてください。また、インターフェースを変更せずにモジュールの内部を変更できます。

例外の処理

スロー、エラー、終了は処理できることを何度か言及しました。これを行う方法は、try ... catch式を使用することです。

try ... catchは、成功した場合だけでなく、発生したエラーも処理しながら式を評価する方法です。このような式の一般的な構文は次のとおりです。

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

tryofの間にあるExpressionは、保護されていると言います。これは、その呼び出し内で発生するあらゆる種類の例外がキャッチされることを意味します。try ... ofcatchの間にあるパターンと式は、case ... ofとまったく同じように動作します。最後に、catch部分:ここで、TypeOfErrorを、この章で見てきたそれぞれのタイプについて、errorthrow、またはexitのいずれかに置き換えることができます。タイプが指定されていない場合は、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はスローのみを受信しています。前述したように、これはタイプが言及されていない場合、スローが想定されるためです。次に、各タイプのキャッチ句を持つ関数があります。

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}

メニューの次の例は、単一の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です。次に、これを追加してさらに良いでしょう。

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."
Monty Python's black knight

9行目の式は、関数実行が正常に行われる場合の黒騎士の通常の動作を示しています。その後に続く各行は、例外をそのクラス(スロー、エラー、終了)およびそれに関連付けられた理由(slicecut_armcut_leg)に従ってパターンマッチングしていることを示しています。

式13と14でここに示されているのは、例外のキャッチオール句です。_:_パターンは、あらゆるタイプの例外を確実にキャッチするために使用する必要があるものです。実際には、キャッチオールパターンを使用する場合は注意が必要です。処理できるものからコードを保護するようにしてください。それ以上は保護しないようにしてください。Erlangには、残りの部分を処理するための他の機能があります。

try ... catchの後に追加できる追加の句もあり、これは常に実行されます。これは、他の多くの言語の「finally」ブロックに相当します。

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

エラーがあるかどうかに関係なく、after部分内の式は必ず実行されます。ただし、after構造から戻り値を取得することはできません。したがって、afterは、副作用のあるコードを実行するために主に使用されます。これの一般的な用途は、例外が発生したかどうかに関係なく、読み取り中のファイルを必ず閉じるようにする場合です。

これで、Erlangの3つのクラスの例外をキャッチブロックで処理する方法がわかりました。ただし、情報を隠していました。実際には、tryofの間に複数の式を持つことが可能です!

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.

exceptions:whoa()を呼び出すと、throw(up)が原因で、明らかに{caught, throw, up}が得られます。したがって、tryofの間には複数の式を持つことができるのは事実です...

exceptions:whoa/0で私が強調したこと、そしてあなたが気づかなかったかもしれないことは、このように多くの式を使用する場合、戻り値が何であるかを必ずしも気にする必要がないということです。したがって、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.

そして、少しすっきりしました!

注: 例外の保護された部分は末尾再帰にできないことを知っておくことが重要です。例外が発生した場合に備えて、VMは常にそこに参照を保持する必要があります。

of部分のないtry ... catch構文は、保護された部分しかないため、そこから再帰関数を呼び出すことは、長時間実行されることを想定したプログラム(Erlangのニッチな部分です)にとって危険な場合があります。十分な反復の後、メモリ不足になるか、プログラムが理由もわからずに遅くなります。再帰呼び出しをofcatchの間に置くことで、保護された部分にはなくなり、ラストコール最適化の恩恵を受けることができます。

一部の人は、明らかに非再帰的なコードで結果が何も使用されない場合を除いて、そのような予期しないエラーを回避するために、デフォルトでtry ... catchではなくtry ... of ... catchを使用します。何をするかについては、自分で判断できる可能性が非常に高いです!

待って、まだあります!

まるでほとんどの言語と同等であるだけでは飽き足らないかのように、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

これを見ると、throwは同じままですが、exitとerrorの両方が{'EXIT', Reason}として表現されていることがわかります。これは、exitの後でerrorが言語に追加されたためです(下位互換性のために類似の表現が維持されました)。

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

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}]}}

クラッシュしたプロセスで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のもう1つの問題は、例外の基になる表現のように見えるものと実際の例外の違いがわからないことです。

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}]}}

また、エラーと実際のexitの違いもわかりません。上記の例外を生成するために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の背後にいるため、関数が例外をスローしたのか、それとも実際の値が返されたのかを判断することはできません。これは実際にはそれほど頻繁には発生しないかもしれませんが、それでもR10Bリリースでtry ... catch構文を追加する理由として十分なほどの欠点です。

トライを木で試してみましょう

例外を実践するために、treeモジュールを掘り下げる必要がある小さな演習を行います。ツリー内で値を検索して、その値がすでに存在するかどうかを調べる関数を追加します。ツリーはキーでソートされており、この場合はキーを気にしないため、値が見つかるまで全体をトラバースする必要があります。

ツリーのトラバーサルは、tree:lookup/2で行ったものとほぼ同様になりますが、今回は常に左ブランチと右ブランチの両方を検索します。関数を記述するには、ツリーノードが{node, {Key, Value, NodeLeft, NodeRight}}であるか、空の場合は{node, 'nil'}であることを覚えておけば十分です。これがあれば、例外のない基本的な実装を記述できます。

%% looks for a given value 'Val' in the tree.
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.

この実装の問題は、ブランチするツリーのすべてのノードが前のブランチの結果をテストする必要があることです

A diagram of the tree with an arrow following every node checked while traversing the tree, and then when returning the result

これは少し面倒です。throwを使用すると、比較回数を減らすことができます

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).

上記のコードの実行は前のバージョンと似ていますが、戻り値をチェックする必要がない点が異なります。戻り値はまったく気にしません。このバージョンでは、throwのみが値が見つかったことを意味します。これが発生すると、ツリーの評価が停止し、最上位のcatchに戻ります。それ以外の場合は、最後のfalseが返されるまで実行が継続され、それがユーザーに表示されるものです

A diagram of the tree with an arrow following every node checked while traversing the tree, and then skipping all the nodes on the way back up (thanks to a throw)

もちろん、上記の実装は前の実装よりも長いです。ただし、実行している操作に応じて、throwを使用した非ローカルリターンを使用することで、速度と明快さを向上させることができます。現在の例は単純な比較であり、見るべきものはあまりありませんが、より複雑なデータ構造と操作では実践が理にかなっています。

とはいえ、シーケンシャルErlangで実際の問題を解決する準備はおそらくできているでしょう。