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)的使用,且听下回分解。