Erlang那些事兒第3回之我是函式(fun),萬物之源MFA

snowcicada發表於2021-01-03

  Erlang程式碼到處都是模式匹配,這把屠龍刀可是Erlang的看家本領、獨家絕學,之前在《Erlang那些事兒第1回之我是變數,一次賦值永不改變》文章提到過,Erlang一切皆是模式匹配。從變數的賦值、函式的形參傳遞、過載函式的應用,到處都有模式匹配的影子,剛開始寫程式碼會感覺不習慣,但是當你用習慣之後,會覺得這個武林祕籍是多麼的好用。但是本回書重點講函式,畢竟以後寫程式碼都會應用到函式fun,早點講方便後面的使用。

  Erlang語言中的函式很強大,同一個邏輯可以用多種寫法,因為一個函式有形參,也有函式內部實現,這2個地方只要合理地應用模式匹配,那麼可以發揮出非常大的作用。本回書會使用模式匹配、關卡、遞迴、apply、引數傳遞在函式中的用法。

知識點1:函式過載

  特點1:形引數量不同;

  特點2:函式之間用點號(.)分隔。

  C++一個很重要的特性就是函式過載,這個特性的必要條件是函式的形引數量必須不一樣。在Erlang程式碼中,這個規則同樣適用,一個同名函式可以有多個不同引數數量的版本,匯出列表也要相應地體現出來,來買個水果試試:

  建立檔案fruit_price01.erl,程式碼如下:

 1 -module(fruit_price01).
 2 -author("snowcicada").
 3 
 4 %% API
 5 -export([fruit_price/0, fruit_price/1]).
 6 
 7 %% 買1個水果的價格
 8 fruit_price() ->
 9     fruit_price(1).
10 
11 %% 買多個水果的價格
12 fruit_price(Count) ->
13     Count * 10.

  上述程式碼存在2個fruit_price函式,一個是0參,一個是1參,如果有需要給其他模組使用的情況下,那麼就要新增到export匯出列表,來執行試一下:

Eshell V11.1.3  (abort with ^G)
1> c(fruit_price01).
{ok,fruit_price01}
2> fruit_price01:fruit_price().
10
3> fruit_price01:fruit_price(10).
100

  在Erlang終端執行函式,不管是0參還是1參都能正常工作。

知識點2:函式形參的模式匹配

  特點1:形引數量相同;

  特點2:函式之間用分號;分隔;

  特點3:從上往下匹配。

  函式的每個形參都可以用一個表示式進行匹配,當傳入的引數匹配上提前寫好的表示式,那麼就進入這個函式執行;如果不匹配的話,那麼要麼報錯,要麼會進入一個能夠匹配的函式分支。

  假設買1個水果沒打折,買2個打8折,買3個以上打5折。建立檔案fruit_price01.erl,程式碼如下:

 1 -module(fruit_price02).
 2 -author("snowcicada").
 3 
 4 %% API
 5 -export([fruit_price/1, discount/2]).
 6 
 7 fruit_price(Count) ->
 8     io:format("~p~n", [discount(Count, 10)]).
 9 
10 %% 函式形參進行模式匹配
11 %% 買1個沒打折,買2個打8折,買3個以上打5折
12 discount(1 = Count, Price) ->
13     Count * Price;
14 discount(2 = Count, Price) ->
15     Count * Price * 0.8;
16 discount(Count, Price) ->
17     Count * Price * 0.5.

  在Erlang語言中,等於號(=)並不是賦值,而是進行了一次模式匹配。所以第12行裡面寫的1 = Count是在匹配,匹配Count是否等於1,如果匹配成功,那麼就會執行第13行的程式碼。第14行同理。

  有趣的是第16行,只寫了Count表示對任何資料都可以匹配成功,既然第12行、14行已經匹配了1和2,那麼當Count等於3或3以上的時候就會執行第17行的程式碼。

知識點3:case...of...end表示式的模式匹配

  特點1:不同的匹配表示式末尾用分號(;)分隔,最後一個匹配表示式不需要加分號(;);

  特點2:下劃線或者一個普通變數可以匹配任何情況;

  特點3:從上往下匹配。

  寫程式碼總不能為了處理不同的情況而每次都寫多個函式匹配,這樣寫起來不一定方便,所以Erlang還提供了case...of...end表示式。接下來使用case表示式來重寫上面的discount函式。

  新增函式discount_case,程式碼如下:

 1 %% 引數模式匹配
 2 %% 買1個沒打折,買2個打8折,買3個以上打5折
 3 discount_case(Count, Price) ->
 4     case Count of
 5         1 ->
 6             Count * Price;
 7         2 ->
 8             Count * Price * 0.8;
 9         _ -> %% 下劃線也可以替換成一個變數,比如N,Cnt,都可以,只要是變數就行
10             Count * Price * 0.5
11     end.

  case...of中間寫的是表示式,of後面可以寫入不一樣的匹配表示式,匹配成功就會執行箭頭後面的語句。

知識點4:函式關卡

  函式外部可以對形參新增一些條件,指定不同的條件執行不同的函式,這裡稱為關卡。

  新增函式discount_guard,程式碼如下:

1 %% 函式形參關卡判斷
2 %% 買1個沒打折,買2個打8折,買3個以上打5折
3 discount_guard(Count, Price) when Count =:= 1 ->
4     Count * Price;
5 discount_guard(Count, Price) when Count =:= 2 ->
6     Count * Price * 0.8;
7 discount_guard(Count, Price) ->
8     Count * Price * 0.5.

  在箭頭(->)前面,使用when關鍵字對形參進行判斷,第3行顯示,當Count等於1的時候,會執行這個函式。

知識點5:if關卡

  同樣還有更方便的關卡方式,就是使用if...end表示式。

  新增函式discount_if,程式碼如下:

 1 %% 引數關卡判斷
 2 %% 買1個沒打折,買2個打8折,買3個以上打5折
 3 discount_if(Count, Price) ->
 4     if
 5         Count =:= 1 ->
 6             Count * Price;
 7         Count =:= 2 ->
 8             Count * Price * 0.8;
 9         true ->
10             Count * Price * 0.5
11     end.

  case和if的差別在於表示式寫的是不是模式匹配,if中間那些表示式是用來判斷是否相等,這種是很明確的相等比較。但是case中間的表示式放的是匹配表示式,而且case...of中間可以寫複雜的表示式。

  以下列出的關卡判斷函式和關卡內建函式,可用於if關卡或者函式外的when關卡。

  關卡判斷函式:

  

  關卡內建函式:

  

   介紹以上4種版本的discount,我們調整下fruit_price函式,

1 fruit_price(Count) ->
2     io:format("~p~n", [discount(Count, 10)]),
3     io:format("~p~n", [discount_case(Count, 10)]),
4     io:format("~p~n", [discount_guard(Count, 10)]),
5     io:format("~p~n", [discount_if(Count, 10)]).

  執行結果:

Erlang/OTP 23 [erts-11.1.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Eshell V11.1.3  (abort with ^G)
1> c(fruit_price02).
{ok,fruit_price01}
2> fruit_price02:fruit_price(10).
50.0
50.0
50.0
50.0
ok

  4個版本的discount執行結果都一樣。

知識點6:函式作為引數傳遞

  這個特性很常見,很多語言都可以把函式作為引數進行傳遞,只是語法有些小差異罷了。不囉嗦,寫個例子吧,建個fruit_price03.erl檔案,程式碼如下:

 1 -module(fruit_price03).
 2 -author("snowcicada").
 3 
 4 %% API
 5 -export([fruit_price/1, discount/2, get_discount_func/0]).
 6 
 7 fruit_price(Count) ->
 8     Discount = get_discount_func(),
 9     io:format("~p~n", [Discount(Count, 10)]).
10 
11 get_discount_func() ->
12     fun discount/2.
13 
14 %% 函式形參進行模式匹配
15 %% 買1個沒打折,買2個打8折,買3個以上打5折
16 discount(1 = Count, Price) ->
17     Count * Price;
18 discount(2 = Count, Price) ->
19     Count * Price * 0.8;
20 discount(Count, Price) ->
21     Count * Price * 0.5.

  discount函式有2個形參,所以Erlang要返回一個函式,就如你所見的第12行,fun discount/2。

 知識點7:遞迴函式

  Erlang函式在處理模式匹配或者關卡的時候,可以有多個分支,就如同知識點2和知識點4的形式。通過這個方式,可以靈活的寫出遞迴函式,對一些臨界情況的處理,這裡寫個簡單的例子就好,以後講到列表的時候會使用到,用得很靈活有趣。

  建立div_three.erl檔案,程式碼如下:

 1 -module(div_three).
 2 -author("snowcicada").
 3 
 4 %% API
 5 -export([print/1]).
 6 
 7 print(N) when N =:= 0 ->
 8     io:format("~n");
 9 print(N) when N rem 3 =:= 0 ->
10     io:format("~p ", [N]),
11     print(N - 1);
12 print(N) ->
13     print(N - 1).

  當N等於0的時候,會執行第7行,函式輸出換行立馬結束;當N對3取餘等於0的時候,執行第9行,可以被3整除的數字將會列印出來,然後繼續呼叫print(N-1),這裡就是遞迴呼叫。

  當執行過了第7、第9行的關卡,剩下的都會執行第12行,這裡什麼都沒處理,直接遞迴呼叫print即可。

  執行結果:

Erlang/OTP 23 [erts-11.1.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Eshell V11.1.3  (abort with ^G)
1> c(div_three).
{ok,div_three}
2> div_three:print(100).
99 96 93 90 87 84 81 78 75 72 69 66 63 60 57 54 51 48 45 42 39 36 33 30 27 24 21 18 15 12 9 6 3
ok

知識點8:使用apply呼叫函式

  MFA是Module、Function、Arguments的縮寫,指模組呼叫函式,傳入形參,格式如:M:F(A),也可以這樣:apply(M, F, A)。在Erlang自帶的標準庫中,MFA的呼叫方式很常見,也是Erlang實現熱更新屢試不爽的步驟之一。其中的A很容易出現低階錯誤,大部分模組的引數支援傳入列表,所以通常的呼叫方式如:M:F([A1, A2, A3])。

  Erlang提供了apply函式,可通過指定模組名、函式名和引數進行呼叫,這裡貼下apply的實現原始碼:

 1 %% Shadowed by erl_bif_types: erlang:apply/2
 2 -spec apply(Fun, Args) -> term() when
 3       Fun :: function(),
 4       Args :: [term()].
 5 apply(Fun, Args) ->
 6     erlang:apply(Fun, Args).
 7 
 8 %% Shadowed by erl_bif_types: erlang:apply/3
 9 -spec apply(Module, Function, Args) -> term() when
10       Module :: module(),
11       Function :: atom(),
12       Args :: [term()].
13 apply(Mod, Name, Args) ->
14     erlang:apply(Mod, Name, Args).

  apply分別有2個形參和3個形參,2個形參的版本是apply(F,A),不用傳入模組名,3個形參的版本是apply(M,F,A),需要指定模組名。

  開啟Erlang終端做個試驗就行,使用io:format來測試列印資訊:

Erlang/OTP 23 [erts-11.1.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Eshell V11.1.3  (abort with ^G)
1> io:format("Name:~page:~p~n",["Lucy", 16]).
Name:"Lucy"age:16
ok
2> apply(io, format, ["Name:~p age:~p~n", [lucy, 16]]).
Name:lucy age:16
ok
3> erlang:apply(io, format, ["Name:~p age:~p~n", [lucy, 16]]). %%也可以指定erlang模組,這樣寫的好處是會有智慧提示
Name:lucy age:16
ok

  函式的應用大概就這些了,雖然簡單,但是這些都是日常很經典的技巧。

 

  本回使用的程式碼已上傳Github:https://github.com/snowcicada/erlang-story/tree/main/story003

  下一回將介紹原子(Atom)的使用,且聽下回分解。

  Erlang那些事兒第3回之我是函式(fun),萬物之源MFA

 

  作者:snowcicada
  本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。

相關文章