Erlang代碼處處都是模式匹配,這把屠龍刀但是Erlang的看家本領、獨家絕學,以前在《Erlang那些事兒第1回之我是變量,一次賦值永不改變》文章提到過,Erlang一切皆是模式匹配。從變量的賦值、函數的形參傳遞、重載函數的應用,處處都有模式匹配的影子,剛開始寫代碼會感受不習慣,可是當你用習慣以後,會以爲這個武林祕籍是多麼的好用。可是本回書重點講函數,畢竟之後寫代碼都會應用到函數fun,早點講方便後面的使用。html
Erlang語言中的函數很強大,同一個邏輯能夠用多種寫法,由於一個函數有形參,也有函數內部實現,這2個地方只要合理地應用模式匹配,那麼能夠發揮出很是大的做用。本回書會使用模式匹配、關卡、遞歸、apply、參數傳遞在函數中的用法。git
特色1:形參數量不一樣;github
特色2:函數之間用點號(.)分隔。shell
C++一個很重要的特性就是函數重載,這個特性的必要條件是函數的形參數量必須不同。在Erlang代碼中,這個規則一樣適用,一個同名函數能夠有多個不一樣參數數量的版本,導出列表也要相應地體現出來,來買個水果試試:app
建立文件fruit_price01.erl,代碼以下:async
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參都能正常工做。測試
特色1:形參數量相同;ui
特色2:函數之間用分號;分隔;atom
特色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行的代碼。
特色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後面能夠寫入不同的匹配表達式,匹配成功就會執行箭頭後面的語句。
函數外部能夠對形參添加一些條件,指定不一樣的條件執行不一樣的函數,這裏稱爲關卡。
新增函數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的時候,會執行這個函數。
一樣還有更方便的關卡方式,就是使用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運行結果都同樣。
這個特性很常見,不少語言均可以把函數做爲參數進行傳遞,只是語法有些小差別罷了。不囉嗦,寫個例子吧,建個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。
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),這裏就是遞歸調用。
當執行過了第七、第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
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)的使用,且聽下回分解。