錯誤與例外

別急!

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

像這樣的章節並沒有適當的安插位置。到目前為止,您學到的東西可能已經讓您遇到錯誤,但還不足以知道如何處理它們。事實上,我們無法在本章中看到所有的錯誤處理機制。這部分是因為 Erlang 有兩種主要的範例:函數式和並行。函數式子集是我從本書一開始就一直在解釋的:參照透明性、遞迴、高階函數等等。並行子集是讓 Erlang 聞名的原因:actor、成千上萬的並行處理程序、監管樹等等。

因為我認為在轉向並行部分之前了解函數式部分至關重要,所以我將在本章中只介紹該語言的函數式子集。如果我們要管理錯誤,我們必須先了解它們。

注意: 儘管 Erlang 包含一些處理函數式程式碼中錯誤的方法,但大多數時候您會被告知讓它崩潰。我在導論中暗示了這一點。讓您可以透過這種方式編寫程式的機制位於該語言的並行部分。

錯誤彙整

錯誤有很多種:編譯時錯誤、邏輯錯誤、執行時錯誤和產生的錯誤。我將在本節重點介紹編譯時錯誤,並在接下來的章節中介紹其他錯誤。

編譯時錯誤通常是語法錯誤:檢查您的函數名稱、語言中的符號(括號、圓括號、句點、逗號)、函數的元數等等。以下是一些常見編譯時錯誤訊息和潛在解決方案的列表,以防您遇到它們:

module.beam:模組名稱 'madule' 與檔案名稱 'module' 不符
您在 -module 屬性中輸入的模組名稱與檔案名稱不符。
./module.erl:2:警告:函數 some_function/0 未使用
您尚未匯出函數,或者使用它的位置名稱或元數錯誤。也有可能您編寫了一個不再需要的函數。檢查您的程式碼!
./module.erl:2:未定義函數 some_function/1
該函數不存在。您在 -export 屬性中或宣告函數時寫錯了名稱或元數。當給定函數無法編譯時也會輸出此錯誤,通常是因為語法錯誤,例如忘記用句點結束函數。
./module.erl:5:語法錯誤:'SomeCharacterOrWord' 之前
發生這種情況的原因有很多,例如未關閉的括號、元組或錯誤的表達式終止(例如用逗號關閉 case 的最後一個分支)。其他原因可能包括在您的程式碼中使用保留的 atom 或 unicode 字元在不同編碼之間轉換時發生奇怪的轉換(我看過它發生!)
./module.erl:5:語法錯誤之前
好吧,這一個肯定沒有那麼明確!當您的行終止不正確時,通常會出現這種情況。這是前一個錯誤的特定情況,所以請注意。
./module.erl:5:警告:此表達式將失敗並產生 'badarith' 例外
Erlang 的一切都與動態類型有關,但請記住,這些類型很強。在這種情況下,編譯器足夠聰明,可以發現您的其中一個算術表達式將會失敗(例如,llama + 5)。但是,它不會發現比這複雜得多的類型錯誤。
./module.erl:5:警告:未使用變數 'Var'
您宣告了一個變數,但之後從未使用過它。這可能是您的程式碼中的錯誤,因此請仔細檢查您所寫的內容。否則,如果覺得名稱有助於使程式碼可讀,則您可能想要將變數名稱切換為 _ 或只是以底線為前綴(類似 _Var)。
./module.erl:5:警告:建立了一個術語,但從未使用過
在您的其中一個函數中,您正在執行某些操作,例如建立列表、宣告元組或匿名函數,但從未將其綁定到變數或傳回它。此警告告訴您您正在做無用的事情或犯了一些錯誤。
./module.erl:5:head 不符
您的函數可能有一個以上的 head,並且每個 head 都具有不同的元數。別忘了不同的元數意味著不同的函數,而且您不能以這種方式交錯函數宣告。當您在另一個函數的 head 子句之間插入函數定義時,也會引發此錯誤。
./module.erl:5:警告:此子句無法匹配,因為第 4 行的前一個子句總是匹配
在模組中定義的函數在一個 catch-all 子句之後定義了一個特定的子句。因此,編譯器可以警告您,您甚至永遠不需要轉到另一個分支。
./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)
        
函數的所有 guard 子句都失敗了,或者沒有一個函數子句的模式匹配。
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 中的特定模式,傳入了錯誤類型的資料,或者需要一個 catch-all 子句!
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 的分支。確保您考慮所有情況或添加 catch-all 的 true 子句可能是您需要的。
badmatch
5> [X,Y] = {4,5}.
** exception error: no match of right hand side value {4,5}
        
每當模式匹配失敗時,就會發生 Badmatch 錯誤。這很可能意味著您正在嘗試執行不可能的模式匹配(如上),嘗試第二次綁定變數,或者只是 = 運算子兩邊不相等的任何東西(這幾乎是導致重新綁定變數失敗的原因!)。請注意,此錯誤有時會發生,因為程式設計師認為 _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 非常相似,因為它是關於使用不正確的參數呼叫函數。這裡的主要區別在於,此錯誤通常是由程式設計師在從函數內部(guard 子句之外)驗證參數後觸發的。我將在本章稍後展示如何拋出此類錯誤。
undef
7> lists:random([1,2,3]).
** exception error: undefined function lists:random/1
        
當您呼叫不存在的函數時,會發生這種情況。請確保該函數已從模組中以正確的元數匯出(如果您是從模組外部呼叫它),並仔細檢查您是否正確輸入了函數名稱和模組名稱。取得此訊息的另一個原因是當模組不在 Erlang 的搜尋路徑中時。預設情況下,Erlang 的搜尋路徑設定為位於目前目錄中。您可以使用 code:add_patha/1code:add_pathz/1 來新增路徑。如果這仍然不起作用,請確保您首先編譯了模組!
badarith
8> 5 + llama.
** exception error: bad argument in an arithmetic expression
     in operator  +/2
        called as 5 + llama
        
當您嘗試執行不存在的算術時,會發生這種情況,例如除以零或在 atom 和數字之間進行運算。
badfun
9> hhfuns:add(one,two).
** exception error: bad function one
in function  hhfuns:add/2
        
發生此錯誤的最常見原因是您將變數用作函數,但該變數的值不是函數。在上面的範例中,我正在使用上一章中的 hhfuns 函數,並使用兩個 atom 作為函數。這不起作用,並且會拋出 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 效率指南 中的系統限制。請注意,其中一些錯誤非常嚴重,足以導致整個虛擬機崩潰。

引發例外

A stop sign

在嘗試監控程式碼執行並防止邏輯錯誤時,盡早觸發執行階段崩潰通常是一個好主意,這樣問題才能及早發現。

Erlang 中有三種例外:errorsthrowsexits。它們都有不同的用途(某種程度上)。

Errors

呼叫 erlang:error(Reason) 將結束目前程序中的執行,並在您捕獲它時包含最後呼叫的函式及其引數的堆疊追蹤。這些是引發上述執行階段錯誤的例外類型。

當您不能期望呼叫程式碼處理剛剛發生的事情時,Errors 是函式停止執行的手段。如果您收到 if_clause 錯誤,您能做什麼?更改程式碼並重新編譯,這就是您可以做的(除了顯示漂亮的錯誤訊息之外)。一個不使用 errors 的例子可能是我們在 遞迴章節 中的樹狀模組。該模組在執行查詢時可能並非總是能在樹狀結構中找到特定的鍵。在這種情況下,期望使用者處理未知的結果是合理的:他們可以使用預設值、檢查是否插入新的值、刪除樹狀結構等等。這時適合返回 {ok, Value} 形式的元組或類似 undefined 的原子,而不是引發 errors。

現在,errors 不僅限於上面的例子。您也可以定義自己的錯誤類型。

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

在這裡,custom_error 無法被 Erlang shell 識別,也沒有像「... 中的錯誤引數」這樣的自訂轉換,但它以相同的方式可用,並且可以由程式設計師以相同的方式處理(我們很快就會看到如何做到)。

Exits

有兩種 exits:「內部」exits 和「外部」exits。內部 exits 是透過呼叫函式 exit/1 觸發的,並使目前程序停止執行。外部 exits 是用 exit/2 呼叫的,並且與 Erlang 並行方面的多個程序有關;因此,我們將主要關注內部 exits,並在稍後訪問外部類型。

內部 exits 與 errors 非常相似。事實上,從歷史上講,它們是相同的,並且只有 exit/1 存在。它們的使用案例大致相同。那麼如何選擇一個?嗯,選擇並不明顯。要了解何時使用一個或另一個,別無選擇,只能開始從遠處觀察 actors 和程序的概念。

在簡介中,我將程序比喻為人們透過郵件交流。這個類比沒有太多要補充的,所以我將轉向圖表和氣泡。

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 則不返回這一事實加強了這一點。如果您要有一個相當大的堆疊追蹤或目前函式有很多參數,將 exit 訊息複製到每個監聽程序意味著複製資料。在某些情況下,這可能變得不切實際。

Throws

throw 是一種用於程式設計師可以預期會處理的情況的例外類別。與 exits 和 errors 相比,它們背後並沒有任何「崩潰那個程序!」的意圖,而是控制流程。當您使用 throws 時,期望程式設計師處理它們,通常最好在使用它們的模組中記錄它們的用法。

引發例外的語法是

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

您可以在其中將 permission_denied 替換為您想要的任何內容(包括 '一切都很好',但這沒有幫助,而且您會失去朋友)。

當進行深度遞迴時,Throws 也可用於非本地返回。一個例子是 ssl 模組,它使用 throw/1 作為將 {error, Reason} 元組推送回頂層函式的方法。然後,此函式只是將該元組返回給使用者。這讓實作者僅針對成功案例進行編寫,並讓一個函式處理所有例外情況。

另一個例子可能是陣列模組,其中有一個查詢函式,如果找不到所需的元素,它可以返回使用者提供的預設值。當找不到元素時,會將值 default 作為例外拋出,並且頂層函式會處理該例外並將其替換為使用者提供的預設值。這讓模組的程式設計師無需將預設值作為查詢演算法每個函式的參數傳遞,而是再次只關注成功案例。

A fish that was caught

作為經驗法則,請嘗試將 throw 的使用限制為單一模組的非本地返回,以便更容易偵錯您的程式碼。這也讓您可以更改模組的內部結構,而無需變更其介面。

處理例外

我已經多次提到可以處理 throws、errors 和 exits。方法是使用 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 替換為 errorthrowexit,以表示本章中看到的每種各自的類型。如果沒有提供類型,則會假設為 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 只接收 throws。如前所述,這是因為當未提及類型時,會假設為 throw。然後我們有每個類型的 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 是一個 BIF,它確保變數 Attack 是 arity 為 0 的函式。然後我們加入這個以做為良好的衡量標準。

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 行的表達式示範了黑騎士的正常行為,當函式執行正常發生時。接下來的每一行都示範了根據例外情況的類別(throw、error、exit)及其相關的原因(slicecut_armcut_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 之間有多個表達式!

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(),我們將獲得顯而易見的 {caught, throw, up},因為 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 ... of ... catch 而不是 try ... 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

從這裡我們可以看到,拋出 (throws) 的行為保持不變,但退出 (exits) 和錯誤 (errors) 都以 {'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}]}}

你也可以在崩潰的進程中呼叫 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 之後,我們永遠無法知道函數是拋出了例外還是返回了實際的值!這在實務中可能不會經常發生,但它仍然是一個足夠大的缺陷,導致 R10B 版本增加了 try ... catch 結構。

試試看樹中的 try

為了將例外付諸實踐,我們將進行一個小練習,需要我們挖掘 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

這有點麻煩。在抛出的幫助下,我們可以做出需要較少比較的東西。

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,這就是使用者所看到的。

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)

當然,上面的實現比先前的實現長。然而,藉由使用帶有抛出的非本地返回,有可能在速度和清晰度方面獲得提升,這取決於你正在執行的操作。目前的範例是一個簡單的比較,沒有太多可看的,但是對於更複雜的資料結構和操作,這種做法仍然有意義。

話雖如此,我們可能已經準備好解決循序 Erlang 中的實際問題了。