測試 function 功能,可以改使用 erlide,因為 function 要放在 module 裡面測試,雖然一視窗編輯 erl 檔,另一邊用 erl 的 c(test_module) 也可以動態更新程式碼,使用 erlide 可以省去一些敲打指令的時間。
子句
io:format
io:format 有幾個格式化參數可以使用
~s 列印字串
~p 以美化方式列印,超過一行會自動分行
~w 列印 terms 的原始型態
~n 換行
%% test_module.erl
-module(test_module).
-export[hello/0].
-export[hello/1].
hello(From) ->
io:format( "~s:Hello world~n", [From] ),
io:format( "~p:Hello world~n", [From] ),
io:format( "~w:Hello world~n", [From] ).
hello() ->
hello("").
1> test_module:hello().
:Hello world
[]:Hello world
[]:Hello world
ok
2> test_module:hello("test").
test:Hello world
"test":Hello world
[116,101,115,116]:Hello world
ok
用 pattern matching 在多個子句中進行條件判斷
函數可以有多個子句,每個字句以 ; 分號隔開,最後以 . 句號結尾。
erlang 會自動由上至下逐一進行 pattern matching,一旦有模式符合了,下面其他的模式就不會再進行比對,如果都不匹配,那就會產生 function_clause 異常。
% test_module.erl
either_or_both(true, B) ->
true;
either_or_both(A, true) ->
true;
either_or_both(false, false) ->
false.
只要符合 pattern,就會套用到該子句的 expressions。
1> test_module:either_or_both(true, 123).
true
2> test_module:either_or_both(true, true).
true
guardian clause
在上面的例子中,test_module:either_or_both(true, 123) 後面的 123 應該是異常的參數,可以附帶 guardian clause: when 增加檢查的條件。
% test_module.erl
either_or_both(true, B) when is_boolean(B) ->
true;
either_or_both(A, true) when is_boolean(A) ->
true;
either_or_both(false, false) ->
false.
1> test_module:either_or_both(true, 123).
** exception error: no function clause matching
test_module:either_or_both(true,123) (test_module.erl, line 17)
2> test_module:either_or_both(true, true).
true
gardian clause 中可以使用 is_boolean()、is_atom()、is_integer() 等等判斷的函數,也可以使用 + - * / ++ ,也可使用部份 BIF,例如 self(),但不能使用自己定義的函數或是其他module裡的函數。
合法的 gardian clause 為
- atom: true
- 其他 term 與已繫結的變數: false
- 呼叫 guard predicate,這一些 BIF
- term 比較結果
- 算術表示式
- boolean 表示式
- short-circuit boolean 表示式: andalso, orelse
guard predicate BIFs:
- is_atom(X)
- is_binary(X)
- is_constant(X)
- is_float(X)
- is_function(X)
- is_function(X, N): X 是否為具有 N 個引數的函數
- is_integer(X)
- is_list(X)
- is_number(X)
- is_pid(X)
- is_port(X)
- is_reference(X)
- is_tuple(X)
- is_record(X, Tag): X 是否為型別為 Tag 的 record
- is_record(X, Tag, N): X 是否為型別為 Tag 的 record, 大小為 N
以下這些都是已經不使用的 guard predicate BIFs
- abs(X)
- element(N,X): X 的元素 N,X 必須為 tuple
- float(X)
- hd(X): list X 的 head element
- length(X): list X 的長度
- node(): 目前的節點
- node(X): X 被建立的節點,X 是 process/identifier/reference/port
- round(X): 將 number X 轉成整數
- self(): 目前 process 的 pid
- size(X): X 的大小,X 可以為 tuple 或 binary
- trunc(X): 截斷 X 成為整數
- tl(X): list X 的尾部
variable scope
erlang 習慣會在 tuple 的第一個元素,以 atom 標記此資料的識別標籤。
變數的值雖然是不可異動的,但作用範圍是在該子句之中,一直到分號或句號就結束。
area({circle, Radius}) ->
Radius * Radius * math:pi();
area({square, Side}) ->
Side * Side;
area({rectangle, Height, Width}) ->
Height * Width.
3> test:area({circle,2.1}).
13.854423602330987
case XX of YYY end.
將剛剛的 area 以 case XX of YYY end. 的方式改寫,程式變得比較精簡,但大多數的programmer習慣使用上面的方式,即使要寫三次 area,普遍認為上面的方式可讀性較高。
area(Shape) ->
case Shape of
{circle, Radius} ->
Radius * Radius * math:pi();
{square, Side} ->
Side * Side;
{rectangle, Height, Width} ->
Height * Width
end.
也可以將剛剛的 either_or_both 以 case of 改寫
either_or_both({A,B}) ->
case {A, B} of
{true, B} when is_boolean(B) ->
true;
{A, true} when is_boolean(A) ->
true;
{false, false} ->
false
end.
if
if 是 case of 的簡化形式,不針對特定值判斷,也不包含 pattern,如果只依靠 guardian clause 進行子句選擇時,就可以使用 if。
因為 if 只是 case of 的簡化,所以可以用 case of 改寫 if。
sign(N) when is_number(N) ->
if
N > 0 -> positive;
N < 0 -> negative;
true -> zero
end.
sign1(N) when is_number(N) ->
case N of
_ when N > 0 -> positive;
_ when N < 0 -> negative;
_ when true -> zero
end.
erlang 沒有 if-then-else
erlang 沒有 if-then-else ,只能用 case of 寫。
test_either_or_both({A,B}) ->
case either_or_both({A,B}) of
true -> io:format("true...");
false -> io:format("false...")
end.
逗號與分號
Erlang 程式段落是由幾個子句構成,子句之間會看到逗號 ( , ) 、分號 ( ; ) 及句號 ( . ) ,一個完整的程式段落是以句號結尾:例如,前面看到的模組定義,以及函數定義。
逗號代表 and ,所以程式段落和防衛式都可以是用逗號分隔很多句子。
分號代表 or ,同一函數的數個規則之間以分號分隔。前面提到的 if .. end 、 case ... end 、 try ... end 、和 receive ... end 等等,許多條件判斷規則之間也是用分號分隔。
and 比 or 有較高優先權,換句話說,就是逗號比分號有較高優先權。
fun
作為現有函數別名的 fun
如果要引用 module 的某個函數,並告知程式其他部份,可以呼叫這個函數,就要建立 fun。
fun test:either_or_both/1 可以指定給變數,或是直接放在 yesno 的呼叫參數中。
yesno(F) ->
case F({true, false}) of
true -> io:format("yes~n");
false -> io:format("no~n")
end.
然後在 erl console 裡面
(erlangotp@yaoclNB)15> H= fun test:either_or_both/1.
#Fun<test.either_or_both.1>
(erlangotp@yaoclNB)16> H({true, false}).
true
(erlangotp@yaoclNB)17> test:yesno(H).
yes
ok
(erlangotp@yaoclNB)18> test:yesno(fun test:either_or_both/1).
yes
ok
匿名函數 lambda
fun () -> 0 end
這就是個最簡單的匿名函數,fun開頭,end 結尾,匿名函數的作用,是要綁訂到變數或是直接當作參數傳給其他函數使用。
(erlangotp@yaoclNB)20> test:yesno(fun ({A,B}) -> A or B end).
yes
ok
lists:map lists:filter
lists:map(F, L) 可以將 fun F 套用在 L 的每一個元素上
lists:filter(P, L) 可以將 L 的每一個元素,以 P(E) 的方式檢查是否為 true,結果為 true 就保留在結果的 list 中
(erlangotp@yaoclNB)23> L=[1,2,3,4].
[1,2,3,4]
(erlangotp@yaoclNB)24> Double = fun(X) -> X*2 end.
#Fun<erl_eval.6.80484245>
(erlangotp@yaoclNB)25> lists:map(Double, L).
[1,4,6,8]
(erlangotp@yaoclNB)27> Even = fun(X) -> (X rem 2) =:=0 end.
#Fun<erl_eval.6.80484245>
(erlangotp@yaoclNB)28> Even(8).
true
(erlangotp@yaoclNB)29> Even(7).
false
(erlangotp@yaoclNB)30> lists:map(Even, L).
[false,true,false,true]
(erlangotp@yaoclNB)31> lists:filter(Even, L).
[2,4]
傳出 fun 的函數
以寫程式的角度來看,這就是要寫出一個可以產生 function 的 fun,用這個 fun 可以產生出很多邏輯類似的 function。
MakeTest 是個產生 function 的 fun
(erlangotp@yaoclNB)32> Fruit = [apple, pear, orange].
[apple,pear,orange]
(erlangotp@yaoclNB)33> MakeTest = fun(L) -> (fun(X) -> lists:member(X,L) end) end.
#Fun<erl_eval.6.80484245>
(erlangotp@yaoclNB)34> IsFruit = MakeTest(Fruit).
#Fun<erl_eval.6.80484245>
(erlangotp@yaoclNB)35> IsFruit(pear).
true
(erlangotp@yaoclNB)36> IsFruit(dog).
false
(erlangotp@yaoclNB)37> lists:filter(IsFruit, [dog, pear, bear, apple]).
[pear,apple]
fun(X) -> X*Times end 是 X 的函數,而 Times 是外面的 fun(Times) 傳進來的。
(erlangotp@yaoclNB)38> Mult = fun(Times) -> (fun(X) -> X*Times end) end.
#Fun<erl_eval.6.80484245>
(erlangotp@yaoclNB)39> Triple = Mult(3).
#Fun<erl_eval.6.80484245>
(erlangotp@yaoclNB)40> Triple(3).
9
這就是一種 closure:closure 通常是指 fun ... end 的內部引用的變數,在 fun 外面進行數值綁定的情況。
自己訂做一個 for 迴圈
當我們呼叫 for(1, 10, F) 時,會跟第二個子句符合,所以會變成
[F(1) | for(2,Max,F) ]
接下來會再往下展開
[F(1), F(2) | for(3,Max,F) ]
持續下去,就會得到這個 list
[F(1), F(2), F(3), ..., F(10) ]
for(Max, Max, F) ->
[F(Max)];
for(I, Max, F) ->
[F(I)| for(I+1, Max, F)].
(erlangotp@yaoclNB)42> test:for(1,10,fun(X) -> X end ).
[1,2,3,4,5,6,7,8,9,10]
(erlangotp@yaoclNB)43> test:for(1,10,fun(X) -> X*2 end ).
[2,4,6,8,10,12,14,16,18,20]
何時使用較高次方的函數
「較高次方的函數」就是剛剛提到的把函數當作參數使用,或是將 fun 當作函數的回傳值,Joe Armstrong 在書本裡說,實務這些技術不常用到。
- lists:map/2、filter/2、partition/2 這些BIF很常用到,幾乎可以認定為 erlang 語言的一部分
- 作者很少寫出像剛剛的 for 迴圈的自訂控制,反而常常會呼叫標準函式庫裏的較高次方函數。
- 作者很少寫出傳出 fun 的函數,寫出100個module,大概只有 1~2 個會使用這個技巧
異常與try/catch
異常可視為函數的另一種返回形式,異常會不斷地往上返回到呼叫者,直到被 catch 或是抵達 process 呼叫的起點(這時 process 便會 crash)為止。
erlang 的異常分三類
error: 執行時異常,在發生除以0的錯誤、pattern matching 失敗、找不到匹配的函數子句時觸發。一旦錯誤造成 process crash,就會紀錄到 erlang error log 中。
erlang:error(Reason)
通常不需要在程式中拋出 error,但在撰寫 library 時,適時拋出 badarg 異常卻是個好習慣。
exit: 通常用於通報「process即將停止」。他會迫使 process crash 的時候,將原因告知其他 processes,因此一般不 catch 這類異常。在 process正常終止時,也會使用 exit,他會命令 process 退出,並通報「任務結束、一切正常」。不管哪一種情況,process 因exit而終止,都不算是意外事件,也不會紀錄到 erlang error log 中。
exit(Reason)
exit(normal) 所拋出的異常不會被捕獲,該process會正常結束。
throw: 此異常用於處理用戶自定義的情況,可以用 throw 來通報你的函數遇到了某種意外(例如文件不存在或遇到了非法輸入),可利用throw來完成非局部返回或是用於跳出深層遞迴。如果process沒有catch此異常,就會轉變成一個原因為 nocatch 的 error,迫使 process 終止並紀錄到 erlang error log 中。
throw(SomeTerm)
使用 try ... of ... catch ... after ... end
try
some_unsafe_function()
catch
oops -> got_throw_oops;
throw:Other -> {got_throw, Other};
exit:Reason -> {got_exit, Reason};
error:Reason ->{got_error, reason}
end
一般狀況下,不應該去 catch exit 與 error,這可能會掩蓋系統的錯誤。
如果需要捕獲所有東西,可以用以下的寫法處理
try Expr
catch
_:_ -> got_some_exception
end
如果要區分,正常情況跟異常情況做不同的處理時,可用以下寫法,在正常情況繼續處理,異常時,列印錯誤訊息並退出。這裡的 of 跟 catch 一樣,無法受到 try 的保護,of 與 catch 裡面的子句的異常,會傳播到 try 表達式之外。
try
some_unsafe_function()
of
0 -> io:format("nothing to do~n");
N -> do_something_with(N)
catch
_:_ -> io:format("somethin wrong~n")
end
after 區塊可確保 try, of, catch 全部都執行過後,才會執行,而且一定會執行。包含在 try 中拋出異常,或是在 of, catch 中拋出了新的異常,這些異常會先儲存起來,在 after 處理過後,再重新被拋出。如果 after 裡面又拋出了異常,拋出的異常就會取代先前的異常,原先的異常會被丟棄。
{ok, FileHandle} = file:open("foo.txt", [read]),
try
do_something_with_file(FileHandle)
after
file:close(FileHandle)
end
範例
generate_exception 產生所有可能的錯誤,catcher用來測試是否可以抓住他們。
-module(try_test).
-export([generate_exception/1, demo1/0]).
generate_exception(1) -> a;
generate_exception(2) -> throw(a);
generate_exception(3) -> exit(a);
generate_exception(4) -> {'EXIT', a};
generate_exception(5) -> erlang:error(a).
demo1() ->
[catcher(I) || I <- [1,2,3,4,5]].
catcher(N) ->
try generate_exception(N) of
Val -> {N, normal, Val}
catch
throw:X -> {N, caught, thrown, X};
exit:X -> {N, caught, exited, X};
error:X -> {N, caught, error, X}
end.
測試
(erlangotp@yaoclNB)14> try_test:demo1().
[{1,normal,a},
{2,caught,thrown,a},
{3,caught,exited,a},
{4,normal,{'EXIT',a}},
{5,caught,error,a}]
改進錯誤訊息
使用 erlang:error 可改進錯誤訊息的品質。
例如呼叫 math:sqrt(-10) 會得到錯誤
1> math:sqrt(-10).
** exception error: an error occurred when evaluating an arithmetic expression
in function math:sqrt/1
called as math:sqrt(-10)
我們可以用一個函數,改進錯誤訊息
sqrt(X) when X<0 ->
erlang:error({squareRootNegativeArgument, X});
sqrt(X) ->
math:sqrt(X).
測試
2> try_test:sqrt(-10).
** exception error: {squareRootNegativeArgument,-10}
in function try_test:sqrt/1 (d:/projectcase/erlang/erlangotp/src/try_test.erl, line 34)
try ... catch 的程式風格
一般當函數沒有狀態時,應該回傳 {ok, Value} 或是 {error, Reason} 這樣的值。
只有兩種方式可以呼叫此函數
case f(X) of
{ok, Val} ->
do_something_with(Val);
{error, Why} ->
%% process error
end.
或是以下這種方式,但會在 f(X) 回傳 {error, ...} 時,傳出一個例外
{ok, Val} = f(X),
do_something_with(Val);
通常我們應該撰寫程式處理自己的錯誤
try myfunc(X)
catch
throw:{thisError, X} -> ...
throw:{someOtherError, X} -> ...
end
stack trace
stack trace 就是異常發生時,從 stack 頂部到所有呼叫的逆向順序列表,呼叫 erlang:get_stacktrace() 可查看目前這個 process 最近拋出的異常的 stack trace。每一個函數都會以 {Module, Function, Args} 的形式表示,其中 Module 與 Function 是 atom,而 Args 可能是函數的元數,或是函數被呼叫時的參數列表。
如果呼叫 erlang:get_stacktrace() 後得到一個空 list,就表示沒有發生任何異常。
重拋異常
檢視異常之後,再判斷是否要進行補捉,必要時可先捕捉異常,再以 erlang:raise(Class, Reason, Stacktrace) 重新拋出。這裡的 Class 必須是 error、exit 或 throw,Stacktrace 則應該來自 erlang:get_stacktrace()。
try
do_something()
catch
Class:Reason ->
Trace = erlang_getstacktrace(),
case analyze_exc(Class, Reason) of
true -> handle_exc(Class, Reason, Trace);
false-> erlang:raise(Class, Reason, Trace)
end
end
舊版 erlang 支援的 catch
catch 在老的程式碼中很常見,寫法為 catch Expression,如果可取得結果,就以此為結果,如果發生異常,就將捕獲的異常作為 catch 的結果。
(erlangotp@yaoclNB)1> catch 2+2.
4
(erlangotp@yaoclNB)2> catch throw(foo).
foo
(erlangotp@yaoclNB)3> catch exit(foo).
{'EXIT',foo}
(erlangotp@yaoclNB)4> catch foo=bar.
{'EXIT',{{badmatch,bar},[{erl_eval,expr,3,[]}]}}
(erlangotp@yaoclNB)5>
對於 error,得到的是包含異常本身與 stacktrace 的 tuple,這種設計看來簡單,但卻讓程式無法判斷究竟發生了什麼,無法進行後續處理,要避免使用舊寫法的 catch。
捕捉例外的另一個方式是直接使用 catch,但是跟前一個例子比較結果後,會發現第二個 throw(a) 的部份沒有捕捉到,而且 3,4,5 的部份,也不能知道精確的資訊。
demo2() ->
[{I, (catch generate_exception(I))} || I <- [1,2,3,4,5] ].
(erlangotp@yaoclNB)15> try_test:demo2().
[{1,a},
{2,a},
{3,{'EXIT',a}},
{4,{'EXIT',a}},
{5,
{'EXIT',{a,[{try_test,generate_exception,1,
[{file,"d:/projectcase/erlang/erlangotp/src/try_test.erl"},
{line,16}]},
{try_test,'-demo2/0-lc$^0/1-0-',1,
[{file,"d:/projectcase/erlang/erlangotp/src/try_test.erl"},
{line,31}]},
{try_test,'-demo2/0-lc$^0/1-0-',1,
[{file,"d:/projectcase/erlang/erlangotp/src/try_test.erl"},
{line,31}]},
{erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,573}]},
{shell,exprs,7,[{file,"shell.erl"},{line,674}]},
{shell,eval_exprs,7,[{file,"shell.erl"},{line,629}]},
{shell,eval_loop,3,[{file,"shell.erl"},{line,614}]}]}}}]
參考
Erlang and OTP in Action
Programming Erlang: Software for a Concurrent World
沒有留言:
張貼留言