9

Erlang那些事儿第3回之我是函数(fun),万物之源MFA

 3 years ago
source link: http://www.cnblogs.com/snowcicada/p/14221215.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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关卡。

关卡判断函数:

QJzIJri.png!mobile

关卡内置函数:

aQNny23.png!mobile

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

Uv6BFvf.png!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK