2014/2/14

erlang basics - function

測試 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 為

  1. atom: true
  2. 其他 term 與已繫結的變數: false
  3. 呼叫 guard predicate,這一些 BIF
  4. term 比較結果
  5. 算術表示式
  6. boolean 表示式
  7. short-circuit boolean 表示式: andalso, orelse

guard predicate BIFs:

  1. is_atom(X)
  2. is_binary(X)
  3. is_constant(X)
  4. is_float(X)
  5. is_function(X)
  6. is_function(X, N): X 是否為具有 N 個引數的函數
  7. is_integer(X)
  8. is_list(X)
  9. is_number(X)
  10. is_pid(X)
  11. is_port(X)
  12. is_reference(X)
  13. is_tuple(X)
  14. is_record(X, Tag): X 是否為型別為 Tag 的 record
  15. is_record(X, Tag, N): X 是否為型別為 Tag 的 record, 大小為 N

以下這些都是已經不使用的 guard predicate BIFs

  1. abs(X)
  2. element(N,X): X 的元素 N,X 必須為 tuple
  3. float(X)
  4. hd(X): list X 的 head element
  5. length(X): list X 的長度
  6. node(): 目前的節點
  7. node(X): X 被建立的節點,X 是 process/identifier/reference/port
  8. round(X): 將 number X 轉成整數
  9. self(): 目前 process 的 pid
  10. size(X): X 的大小,X 可以為 tuple 或 binary
  11. trunc(X): 截斷 X 成為整數
  12. 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 在書本裡說,實務這些技術不常用到。

  1. lists:map/2、filter/2、partition/2 這些BIF很常用到,幾乎可以認定為 erlang 語言的一部分
  2. 作者很少寫出像剛剛的 for 迴圈的自訂控制,反而常常會呼叫標準函式庫裏的較高次方函數。
  3. 作者很少寫出傳出 fun 的函數,寫出100個module,大概只有 1~2 個會使用這個技巧

異常與try/catch

異常可視為函數的另一種返回形式,異常會不斷地往上返回到呼叫者,直到被 catch 或是抵達 process 呼叫的起點(這時 process 便會 crash)為止。

erlang 的異常分三類

  1. error: 執行時異常,在發生除以0的錯誤、pattern matching 失敗、找不到匹配的函數子句時觸發。一旦錯誤造成 process crash,就會紀錄到 erlang error log 中。

     erlang:error(Reason)

    通常不需要在程式中拋出 error,但在撰寫 library 時,適時拋出 badarg 異常卻是個好習慣。

  2. exit: 通常用於通報「process即將停止」。他會迫使 process crash 的時候,將原因告知其他 processes,因此一般不 catch 這類異常。在 process正常終止時,也會使用 exit,他會命令 process 退出,並通報「任務結束、一切正常」。不管哪一種情況,process 因exit而終止,都不算是意外事件,也不會紀錄到 erlang error log 中。

     exit(Reason)

    exit(normal) 所拋出的異常不會被捕獲,該process會正常結束。

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