首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

2.顺序编程 | 2. Sequential Programming

2.1 Erlang Shell

大多数操作系统都有一个命令解释器或shell,UNIX和Linux有很多,Windows有命令提示符。Erlang有自己的shell,可以直接编写Erlang代码,并对其进行评估以查看会发生什么(请参阅shell(3)STDLIB中的手册页)。

通过在操作系统中启动shell或命令解释器并键入,启动Erlang shell(在Linux或UNIX中)erl。你会看到这样的东西。

代码语言:javascript
复制
% erl
Erlang R15B (erts-5.9.1) [source] [smp:8:8] [rq:8] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.9.1  (abort with ^G)
1>

键入“2 + 5.”在shell中,然后按Enter(回车)。注意,此时你已经用句号和回车告诉shell代码已经输入完成。

代码语言:javascript
复制
1> 2 + 5.
7
2>

如图所示,Erlang shell为可输入的行编号(如1> 2>),并且它正确地表示2 + 5是7.如果在shell中写入错误,则可以使用退格键删除,就像大多数炮弹一样。在shell中有更多的编辑命令(参见tty - A command line interfaceERTS用户指南)。

(请注意,以下示例中的shell提供的许多行号是无序的,这是因为本教程是在单独的会话中编写和代码测试的)。

这里有一个更复杂的计算:

代码语言:javascript
复制
2> (42 + 77) * 66 / 3.
2618.0

注意使用括号,乘法运算符“*”和除法运算符“/”,如在正常算术中(见Expressions)。

按Control-C关闭Erlang系统和Erlang外壳。

如下所示:

代码语言:javascript
复制
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
a
%

键入“a”以离开Erlang系统。

另一种关闭Erlang系统的方法是输入halt()*

代码语言:javascript
复制
3> halt().
% 

2.2模块和函数

如果你只能从shell运行代码,那么编程语言没什么用处。所以这里是一个小Erlang程序。将其输入到tut.erl使用合适的文本编辑器命名的文件中。文件名tut.erl非常重要,并且它与您所在的目录位于同一个目录中erl)。如果幸运的话,你的编辑器有一个Erlang模式,可以让你更容易地输入和格式化你的代码(参见The Erlang mode for Emacs工具用户指南),但是你可以在没有的情况下管理完美。这里是输入的代码:

代码语言:javascript
复制
-module(tut).
-export([double/1]).

double(X) ->
    2 * X.

不难猜测,这个方法将数字的值翻倍。代码的前两行将在后面提及。现在让我们编译程序。这可以在Erlang shell中完成,如下所示,其中c意味着编译:

代码语言:javascript
复制
3> c(tut).
{ok,tut}

{ok,tut}意味着编译是可以的。如果它显示“错误”,则表示您输入的文本中存在一些错误。附加的错误消息给出了错误的概念,以便您可以修改文本,然后尝试再次编译该程序。

现在运行程序:

代码语言:javascript
复制
4> tut:double(10).
20

正如预期的那样,10倍是20倍。

现在让我们回到代码的前两行。Erlang程序是用文件编写的。每个文件都包含一个Erlang 模块。模块中的第一行代码是模块名称(请参阅Modules):

代码语言:javascript
复制
-module(tut).

因此,该模块被称为tut。注意句号“。” 在行末。用于存储模块的文件必须与模块名称相同,但扩展名为“.erl”。在这种情况下,文件名是tut.erl。在另一个模块中使用某个功能时,将module_name:function_name(arguments)使用该语法。所以下面的意思就是doubletut参数“10”的模块中调用函数。

代码语言:javascript
复制
4> tut:double(10).

第二行是模块tut包含一个名为double,这需要一个参数%28X

代码语言:javascript
复制
-export([double/1]).

第二行还说可以从模块外部调用此函数。tut稍后再谈这个问题。再次注意行尾的“.”。

现在来看一个更复杂的例子,一个数的阶乘。例如,4的阶乘是4 * 3 * 2 * 1,等于24。

在名为tut1.erl*

代码语言:javascript
复制
-module(tut1).
-export([fac/1]).

fac(1) ->
    1;
fac(N) ->
    N * fac(N - 1).

所以这是一个模块,叫做tut1包含一个叫做函数的模块fac>,它接受一个参数N

第一部分认为1的阶乘为1:

代码语言:javascript
复制
fac(1) ->
    1;

请注意,这部分以分号“;”结尾 这表明有更多的功能fac>可以来。

第二部分说N的因子是N乘以N - 1的因子:

代码语言:javascript
复制
fac(N) ->
    N * fac(N - 1).

注意,这部分以“。”结尾。说这个功能没有更多的部分。

编译文件:

代码语言:javascript
复制
5> c(tut1).
{ok,tut1}

现在计算4的阶乘。

代码语言:javascript
复制
6> tut1:fac(4).
24

这里fac>模块中的函数tut1是用参数调用的4

一个函数可以有很多参数。让我们用扩展tut1两个数字的函数来扩展模块:

代码语言:javascript
复制
-module(tut1).
-export([fac/1, mult/2]).

fac(1) ->
    1;
fac(N) ->
    N * fac(N - 1).

mult(X, Y) ->
    X * Y.

请注意,还需要扩展带有两个参数的-export另一个函数的信息mult

汇编:

代码语言:javascript
复制
7> c(tut1).
{ok,tut1}

尝试新的功能mult

代码语言:javascript
复制
8> tut1:mult(3,4).
12

在这个例子中,数字是整数,参数的功能代码NX以及Y被称为变量。变量必须以大写字母开头(请参阅Variables)。变量的例子是NumberShoeSizeAge

2.3原子

Atom是Erlang的另一种数据类型。原子开始用小信(见Atom),例如charlescentimeterinch。原子只是名字,没有别的。它们不像变量,它可以具有价值。

在名为的文件中输入下一个程序tut2.erl)。从英寸转换为厘米可能会有用,反之亦然:

代码语言:javascript
复制
-module(tut2).
-export([convert/2]).

convert(M, inch) ->
    M / 2.54;

convert(N, centimeter) ->
    N * 2.54.

汇编:

代码语言:javascript
复制
9> c(tut2).
{ok,tut2}

测试:

代码语言:javascript
复制
10> tut2:convert(3, inch).
1.1811023622047243
11> tut2:convert(7, centimeter).
17.78

注意引入小数(浮点数),没有任何解释。希望你能解决这个问题。

让我们看看如果不是centimeterinch被输入到convert职能:

代码语言:javascript
复制
12> tut2:convert(3, miles).
** exception error: no function clause matching tut2:convert(3,miles) (tut2.erl, line 4)

convert函数的两部分称为它的子句。如图所示,miles不属于任何一个条款的一部分。Erlang系统无法匹配任何一个子句,因此function_clause返回错误消息。shell很好地格式化错误消息,但是错误元组保存在shell的历史列表中,并且可以通过shell命令输出v/1

代码语言:javascript
复制
13> v(12).
{'EXIT',{function_clause,[{tut2,convert,
                                [3,miles],
                                [{file,"tut2.erl"},{line,4}]},
                          {erl_eval,do_apply,5,[{file,"erl_eval.erl"},{line,482}]},
                          {shell,exprs,7,[{file,"shell.erl"},{line,666}]},
                          {shell,eval_exprs,7,[{file,"shell.erl"},{line,621}]},
                          {shell,eval_loop,3,[{file,"shell.erl"},{line,606}]}]}}

2.4元组

现在tut2程序不是很好的编程风格。考虑:

代码语言:javascript
复制
tut2:convert(3, inch).

这是否意味着3英寸?或者这是否意味着3以厘米为单位并将转换为英寸?Erlang有一种将事物分组在一起以使事情更容易理解的方法。这些被称为元组,并被大括号包围,“{”和“}”。

所以,{inch,3}表示3英寸,{centimeter,5}表示5厘米。现在让我们写一个新的程序,将厘米转换为英寸,反之亦然。在名为tut3.erl)的文件中输入以下代码:

代码语言:javascript
复制
-module(tut3).
-export([convert_length/1]).

convert_length({centimeter, X}) ->
    {inch, X / 2.54};
convert_length({inch, Y}) ->
    {centimeter, Y * 2.54}.

编译和测试:

代码语言:javascript
复制
14> c(tut3).
{ok,tut3}
15> tut3:convert_length({inch, 5}).
{centimeter,12.7}
16> tut3:convert_length(tut3:convert_length({inch, 5})).
{inch,5.0}

请注意,第16行将5英寸转换为厘米并再次返回,并确保恢复原始值。也就是说,函数的参数可能是另一个函数的结果。考虑第16行(上面)的工作原理。赋予该函数的参数{inch,5}首先与第一个头句子相匹配convert_length,即convert_length({centimeter,X})。可以看出{centimeter,X}不匹配{inch,5}(头部是“ - >”之前的位)。这个失败了,让我们尝试下一个子句的头,即convert_length({inch,Y})。这匹配,并Y得到值5。

元组可以有两个以上的部分,实际上可以包含任意多个部分,并且包含任何有效的Erlang 术语。例如,为了表示世界各城市的温度:

代码语言:javascript
复制
{moscow, {c, -10}}
{cape_town, {f, 70}}
{paris, {f, 28}}

元组中有固定数量的项目。元组中的每个元素称为元素。在元组中{moscow,{c,-10}},元素1 moscow和元素2是{c,-10}。这里c代表摄氏和f华氏。

2.5列表

而元组将事物分组在一起,它也需要表示事物的列表。Erlang中的列表由方括号“”和“”包围。例如,世界各城市的气温列表可以是:

代码语言:javascript
复制
[{moscow, {c, -10}}, {cape_town, {f, 70}}, {stockholm, {c, -4}},
 {paris, {f, 28}}, {london, {f, 36}}]

注意,这个列表太长了,不能放在一行上。这并不重要,Erlang允许在所有“合理的地方”断线,但不允许,例如,在原子、整数和其他原子中间。

查看列表部分的一种有用方法是使用“”。这最好用一个使用shell的例子来解释:

代码语言:javascript
复制
17> [First |TheRest] = [1,2,3,4,5].
[1,2,3,4,5]
18> First.
1
19> TheRest.
[2,3,4,5]

将列表的第一个元素与列表的其余部分分开|使用。First已经得到价值1并且TheRest已经得到价值2,3,4,5。

另一个例子:

代码语言:javascript
复制
20> [E1, E2 | R] = [1,2,3,4,5,6,7].
[1,2,3,4,5,6,7]
21> E1.
1
22> E2.
2
23> R.
[3,4,5,6,7]

在这里,您可以看到|从列表中获取三元素。如果您试图从列表中获取比列表中的元素更多的元素,则会返回一个错误。还请注意没有元素的列表的特殊情况,[]:

代码语言:javascript
复制
24> [A, B | C] = [1, 2].
[1,2]
25> A.
1
26> B.
2
27> C.
[]

在前面的例子中,新的变量名来代替重复使用旧的,: ,FirstTheRestE1E2RABC。原因是一个变量只能在其上下文(范围)中赋值一次。稍后再详细介绍。

以下示例显示如何查找列表的长度。在名为的文件中输入以下代码tut4.erl):

代码语言:javascript
复制
-module(tut4).

-export([list_length/1]).

list_length([]) ->
    0;    
list_length([First | Rest]) ->
    1 + list_length(Rest).

编译和测试:

代码语言:javascript
复制
28> c(tut4).
{ok,tut4}
29> tut4:list_length([1,2,3,4,5,6,7]).
7

说明:

代码语言:javascript
复制
list_length([]) ->
    0;

空列表的长度显然是0。

代码语言:javascript
复制
list_length([First | Rest]) ->
    1 + list_length(Rest).

具有第一个元素First和其余元素的列表Rest的长度是1 +的长度Rest

(仅限高级读者:这不是尾递归,有更好的方法来编写这个函数。)

通常,在其他语言中使用“记录”或“结构”的地方使用元组。而且,列表用于表示具有不同大小的事物,也就是说,链接列表在其他语言中使用。

Erlang没有字符串数据类型。相反,字符串可以用Unicode字符列表表示。这意味着例如该列表[97,98,99]等同于“abc”。Erlang shell是“聪明的”,并猜测你的意思是什么,并以它认为最合适的形式输出它,例如:

代码语言:javascript
复制
30> [97,98,99].
"abc"

2.6地图

地图是一组价值关联的关键。这些关联被封装为“#{”和“}”。要创建从“键”到值42的关联:

代码语言:javascript
复制
> #{ "key" => 42 }.
#{"key" => 42}

让我们用一个例子直接跳到深层,使用一些有趣的特性。

以下示例显示如何使用贴图来引用颜色和Alpha通道来计算Alpha混合。在名为的文件中输入代码color.erl):

代码语言:javascript
复制
-module(color).

-export([new/4, blend/2]).

-define(is_channel(V), (is_float(V) andalso V >= 0.0 andalso V =< 1.0)).

new(R,G,B,A) when ?is_channel(R), ?is_channel(G),
                  ?is_channel(B), ?is_channel(A) ->
    #{red => R, green => G, blue => B, alpha => A}.

blend(Src,Dst) ->
    blend(Src,Dst,alpha(Src,Dst)).

blend(Src,Dst,Alpha) when Alpha > 0.0 ->
    Dst#{
        red   := red(Src,Dst) / Alpha,
        green := green(Src,Dst) / Alpha,
        blue  := blue(Src,Dst) / Alpha,
        alpha := Alpha
    };
blend(_,Dst,_) ->
    Dst#{
        red   := 0.0,
        green := 0.0,
        blue  := 0.0,
        alpha := 0.0
    }.

alpha(#{alpha := SA}, #{alpha := DA}) ->
    SA + DA*(1.0 - SA).

red(#{red := SV, alpha := SA}, #{red := DV, alpha := DA}) ->
    SV*SA + DV*DA*(1.0 - SA).
green(#{green := SV, alpha := SA}, #{green := DV, alpha := DA}) ->
    SV*SA + DV*DA*(1.0 - SA).
blue(#{blue := SV, alpha := SA}, #{blue := DV, alpha := DA}) ->
    SV*SA + DV*DA*(1.0 - SA).

编译和测试:

代码语言:javascript
复制
> c(color).
{ok,color}
> C1 = color:new(0.3,0.4,0.5,1.0).
#{alpha => 1.0,blue => 0.5,green => 0.4,red => 0.3}
> C2 = color:new(1.0,0.8,0.1,0.3).
#{alpha => 0.3,blue => 0.1,green => 0.8,red => 1.0}
> color:blend(C1,C2).
#{alpha => 1.0,blue => 0.5,green => 0.4,red => 0.3}
> color:blend(C2,C1).
#{alpha => 1.0,blue => 0.38,green => 0.52,red => 0.51}

这个例子有必要作一些解释:

代码语言:javascript
复制
-define(is_channel(V), (is_float(V) andalso V >= 0.0 andalso V =< 1.0)).

首先is_channel定义一个宏来帮助进行警卫测试。这只是为了方便和减少语法混乱。有关宏的更多信息,请参阅The Preprocessor

代码语言:javascript
复制
new(R,G,B,A) when ?is_channel(R), ?is_channel(G),
                  ?is_channel(B), ?is_channel(A) ->
    #{red => R, green => G, blue => B, alpha => A}.

该函数new/4创建一个新的地图术语并让键redgreenblue,和alpha与初始值相关联。在这种情况下,只允许包含0.0和1.0之间的浮点值,这由?is_channel/1每个参数的宏保证。=>创建新地图时只允许操作员。

通过调用blend/2创建的任何颜色术语new/4,可以根据两个映射术语来确定结果颜色。

第一件事blend/2是用来计算得到的alpha通道:

代码语言:javascript
复制
alpha(#{alpha := SA}, #{alpha := DA}) ->
    SA + DA*(1.0 - SA).

alpha使用:=运算符为两个参数提取与键关联的值。地图中的其他键将被忽略,只alpha需要键并检查。

这也是red/2blue/2green/2函数的情况下。

代码语言:javascript
复制
red(#{red := SV, alpha := SA}, #{red := DV, alpha := DA}) ->
    SV*SA + DV*DA*(1.0 - SA).

这里的区别在于,在每个映射参数中检查两个键。其他键将被忽略。

最后,让我们在blend/3*

代码语言:javascript
复制
blend(Src,Dst,Alpha) when Alpha > 0.0 ->
    Dst#{
        red   := red(Src,Dst) / Alpha,
        green := green(Src,Dst) / Alpha,
        blue  := blue(Src,Dst) / Alpha,
        alpha := Alpha
    };

Dst图与新的信道值来更新。用新值更新现有密钥的语法是在:=操作符中。

2.7标准模块和手册页

Erlang有许多标准模块来帮助你做事。例如,该模块io包含许多帮助进行格式化输入/输出的功能。要查找有关标准模块的信息,erl -man可以在运行shell或命令提示符下使用该命令(与您启动的位置相同erl)。尝试操作系统shell命令:

代码语言:javascript
复制
% erl -man io
ERLANG MODULE DEFINITION                                    io(3)

MODULE
     io - Standard I/O Server Interface Functions

DESCRIPTION
     This module provides an  interface  to  standard  Erlang  IO
     servers. The output functions all return ok if they are suc-
     ...

如果这不适用于您的系统,则该文档将作为HTML在Erlang / OTP版本中提供。您也可以将文档作为HTML阅读或从www.erlang.se(商业Erlang)或www.erlang.org(开放源代码)网站下载为PDF格式。例如,对于Erlang / OTP版本R9B:

代码语言:javascript
复制
http://www.erlang.org/doc/r9b/doc/index.html

2.8写入终端输出

能够在示例中执行格式化输出是很好的,所以下一个示例显示了使用该io:format函数的简单方法。像所有其他导出的函数一样,您可以io:format在shell中测试函数:

代码语言:javascript
复制
31> io:format("hello world~n", []).
hello world
ok
32> io:format("this outputs one Erlang term: ~w~n", [hello]).
this outputs one Erlang term: hello
ok
33> io:format("this outputs two Erlang terms: ~w~w~n", [hello, world]).
this outputs two Erlang terms: helloworld
ok
34> io:format("this outputs two Erlang terms: ~w ~w~n", [hello, world]).
this outputs two Erlang terms: hello world
ok

该函数format/2(即format有两个参数)需要两个列表。第一个几乎总是列在“”之间的列表。该列表按原样打印出来,除了每个?w由第二个列表中的顺序替换。每个?n被一个新行代替。该io:format/2函数本身返回原子ok,如果一切按计划进行。像Erlang中的其他函数一样,如果发生错误,它会崩溃。这在Erlang中并不是一个错误,这是一个蓄意的政策。Erlang具有复杂的机制来处理稍后显示的错误。作为练习,尽量让它io:format崩溃,这不应该很难。但请注意,虽然io:format崩溃,但Erlang shell本身不会崩溃。

2.9更大的例子

现在以一个更大的例子来巩固你迄今为止学到的东西。假设你有一个来自世界上许多城市的温度读数列表。其中一些是摄氏温度,一些是华氏温度(如前面的列表)。首先让我们将它们全部转换为摄氏温度,然后让我们整齐地打印数据。

代码语言:javascript
复制
%% This module is in file tut5.erl

-module(tut5).
-export([format_temps/1]).

%% Only this function is exported
format_temps([])->                        % No output for an empty list
    ok;
format_temps([City | Rest]) ->
    print_temp(convert_to_celsius(City)),
    format_temps(Rest).

convert_to_celsius({Name, {c, Temp}}) ->  % No conversion needed
    {Name, {c, Temp}};
convert_to_celsius({Name, {f, Temp}}) ->  % Do the conversion
    {Name, {c, (Temp - 32) * 5 / 9}}.

print_temp({Name, {c, Temp}}) ->
    io:format("~-15w ~w c~n", [Name, Temp]).
代码语言:javascript
复制
35> c(tut5).
{ok,tut5}
36> tut5:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          -10 c
cape_town       21.11111111111111 c
stockholm       -4 c
paris           -2.2222222222222223 c
london          2.2222222222222223 c
ok

在查看该程序的工作方式之前,请注意,代码中添加了一些注释。注释以%字符开始,并继续行尾。另请注意,该-export([format_temps/1]).行只包含该功能format_temps/1。其他功能是本地功能,也就是说,它们在模块外部是不可见的tut5

还要注意,当从shell中测试程序时,由于行太长,输入分散在两行中。

format_temps第一次被调用时,City获取该值{moscow,{c,-10}}并且Rest是列表的其余部分。所以这个函数print_temp(convert_to_celsius({moscow,{c,-10}}))被调用。

这是一个函数调用,convert_to_celsius({moscow,{c,-10}})作为函数的参数print_temp。当函数调用像这样嵌套时,它们从内到外执行(评估)。也就是说,首先convert_to_celsius({moscow,{c,-10}})进行评估,{moscow,{c,-10}}由于温度已经是摄氏温度,因此给出该值。然后print_temp({moscow,{c,-10}})进行评估。该函数的convert_to_celsius工作方式convert_length与上例中的函数类似。

print_temp只需io:format以类似于上述内容的方式进行调用即可。注意?-15w表示打印字段长度(宽度)为15的“term”,并将其左对齐。(请参阅io(3))STDLIB手册页。

现在format_temps(Rest)以列表的其余部分作为参数进行调用。这种做法与其他语言的循环结构类似。(是的,这是递归,但不要让你担心。)所以同样的format_temps函数再次被调用,这次City得到的值{cape_town,{f,70}}和重复的过程与之前一样。这样做直到列表变空,即[],这会导致第一个子句format_temps([])匹配。这只是返回(结果)原子ok,所以程序结束。

2.10匹配、保护和变量范围

找到像这样的列表中的最大和最小温度是很有用的。在扩展程序来完成此操作之前,让我们看一下用于查找列表中元素的最大值的函数:

代码语言:javascript
复制
-module(tut6).
-export([list_max/1]).

list_max([Head|Rest]) ->
   list_max(Rest, Head).

list_max([], Res) ->
    Res;
list_max([Head|Rest], Result_so_far) when Head > Result_so_far ->
    list_max(Rest, Head);
list_max([Head|Rest], Result_so_far)  ->
    list_max(Rest, Result_so_far).
代码语言:javascript
复制
37> c(tut6).
{ok,tut6}
38> tut6:list_max([1,2,3,4,5,7,4,3,2,1]).
7

首先注意两个函数具有相同的名称,list_max。但是,每个参数都需要不同数量的参数(参数)。在Erlang中,这些被视为完全不同的功能。在这种情况下list_max/1,需要区分这些函数的地方,你写了Name/Arity,其中Name是函数名,Arity是参数的个数list_max/2

在这个例子中,你会看到一个“携带”一个值的列表,在这种情况下Result_so_farlist_max/1只是假定列表的最大值是列表的头部,并list_max/2与列表的其余部分以及列表头部的值一起调用。在上面这将是list_max([2,3,4,5,7,4,3,2,1],1)。如果您尝试使用list_max/1空列表或试图将其用于不属于列表的任何内容,则会导致错误。注意Erlang的理念不是在它们发生的函数中处理这种类型的错误,而是在其他地方这样做。稍后再详细介绍。

在中list_max/2,你沿着列表走下去,Head而不是Result_so_farHead> 时使用Result_so_farwhen是在函数中的 - >之前使用的一个特殊词,表示如果下面的测试是正确的,则只使用该函数的这一部分。这种类型的测试称为警卫。如果警卫是假的(即警卫失败),则尝试下一部分功能。在这种情况下,如果Head不大于Result_so_far,那么它必须小于或等于它。这意味着该功能的下一部分不需要警卫。

一些有用的警卫操作人员是:

  • < less than
  • greater than
  • == equal
  • = greater or equal
  • =< less or equal
  • /= not equal

(见Guard Sequences)。

要将上述程序更改为计算列表中元素的最小值的程序,您只需写入<而不是>。(但将函数的名称更改为明智的做法是明智的list_min。)

早些时候有人提到一个变量只能在其范围内赋值一次。在上面你看到Result_so_far有几个值。这是可以的,因为每次你打电话给list_max/2你创建一个新的范围,并且可以把它Result_so_far看作每个范围内的一个不同的变量。

另一种创建和赋予变量值的方法是使用匹配运算符=。因此,如果您编写M = 5了一个名为M5 的变量,则会创建值为5.如果在相同范围内,则会写入M = 6,则会返回错误。在shell中试试这个:

代码语言:javascript
复制
39> M = 5.
5
40> M = 6.
** exception error: no match of right hand side value 6
41> M = M + 1.
** exception error: no match of right hand side value 6
42> N = M + 1.
6

使用Match运算符尤其有助于分离Erlang项和创建新的项。

代码语言:javascript
复制
43> {X, Y} = {paris, {f, 28}}.
{paris,{f,28}}
44> X.
paris
45> Y.
{f,28}

这里X得到的价值parisY{f,28}

如果您再次尝试对另一个城市执行相同的操作,则会返回一个错误:

代码语言:javascript
复制
46> {X, Y} = {london, {f, 36}}.
** exception error: no match of right hand side value {london,{f,36}}

变量也可以用来提高程序的可读性。例如,在list_max/2上面的函数中,你可以写:

代码语言:javascript
复制
list_max([Head|Rest], Result_so_far) when Head > Result_so_far ->
    New_result_far = Head,
    list_max(Rest, New_result_far);

这可能会更清楚一些。

2.11更多关于名单

请记住| 操作符可以用来获取列表的头部:

代码语言:javascript
复制
47> [M1|T1] = [paris, london, rome].
[paris,london,rome]
48> M1.
paris
49> T1.
[london,rome]

该运算符还可用于向列表中添加一个头:

代码语言:javascript
复制
50> L1 = [madrid | T1].
[madrid,london,rome]
51> L1.
[madrid,london,rome]

现在,在处理列表时,有一个这样的例子--颠倒列表的顺序:

代码语言:javascript
复制
-module(tut8).

-export([reverse/1]).

reverse(List) ->
    reverse(List, []).

reverse([Head | Rest], Reversed_List) ->
    reverse(Rest, [Head | Reversed_List]);
reverse([], Reversed_List) ->
    Reversed_List.
代码语言:javascript
复制
52> c(tut8).
{ok,tut8}
53> tut8:reverse([1,2,3]).
[3,2,1]

考虑如何Reversed_List构建。它从[]开始,然后连续将头从列表中取出并反转并添加到列表中Reversed_List,如下所示:

代码语言:javascript
复制
reverse([1|2,3], []) =>
    reverse([2,3], [1|[]])

reverse([2|3], [1]) =>
    reverse([3], [2|[1])

reverse([3|[]], [2,1]) =>
    reverse([], [3|[2,1]])

reverse([], [3,2,1]) =>
    [3,2,1]

该模块lists包含许多用于处理列表的功能,例如,用于反转它们。因此,在编写列表操作函数之前,最好检查是否已经编写了一个函数(参见lists(3)STDLIB中的手册页)。

现在让我们回到城市和温度,但这次采取更加结构化的方法。首先让我们将整个列表转换为摄氏温度,如下所示:

代码语言:javascript
复制
-module(tut7).
-export([format_temps/1]).

format_temps(List_of_cities) ->
    convert_list_to_c(List_of_cities).

convert_list_to_c([{Name, {f, F}} | Rest]) ->
    Converted_City = {Name, {c, (F -32)* 5 / 9}},
    [Converted_City | convert_list_to_c(Rest)];

convert_list_to_c([City | Rest]) ->
    [City | convert_list_to_c(Rest)];

convert_list_to_c([]) ->
    [].

测试功能:

代码语言:javascript
复制
54> c(tut7).
{ok, tut7}.
55> tut7:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
[{moscow,{c,-10}},
 {cape_town,{c,21.11111111111111}},
 {stockholm,{c,-4}},
 {paris,{c,-2.2222222222222223}},
 {london,{c,2.2222222222222223}}]

说明:

代码语言:javascript
复制
format_temps(List_of_cities) ->
    convert_list_to_c(List_of_cities).

这里format_temps/1调用convert_list_to_c/1convert_list_to_c/1起飞的头List_of_cities,它如果需要转换为摄氏温度。| 运算符用于添加(可能)转换为已转换的列表的其余部分:

代码语言:javascript
复制
[Converted_City | convert_list_to_c(Rest)];

或:

代码语言:javascript
复制
[City | convert_list_to_c(Rest)];

这是在到达列表的末尾之前完成的,也就是说,列表是空的:

代码语言:javascript
复制
convert_list_to_c([]) ->
    [].

现在,在转换列表时,添加了一个打印它的函数:

代码语言:javascript
复制
-module(tut7).
-export([format_temps/1]).

format_temps(List_of_cities) ->
    Converted_List = convert_list_to_c(List_of_cities),
    print_temp(Converted_List).

convert_list_to_c([{Name, {f, F}} | Rest]) ->
    Converted_City = {Name, {c, (F -32)* 5 / 9}},
    [Converted_City | convert_list_to_c(Rest)];

convert_list_to_c([City | Rest]) ->
    [City | convert_list_to_c(Rest)];

convert_list_to_c([]) ->
    [].

print_temp([{Name, {c, Temp}} | Rest]) ->
    io:format("~-15w ~w c~n", [Name, Temp]),
    print_temp(Rest);
print_temp([]) ->
    ok.
代码语言:javascript
复制
56> c(tut7).
{ok,tut7}
57> tut7:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          -10 c
cape_town       21.11111111111111 c
stockholm       -4 c
paris           -2.2222222222222223 c
london          2.2222222222222223 c
ok

现在必须添加一个函数才能找到温度最高和最低的城市。当你四次浏览城市列表时,以下程序并不是最有效的方式。但最好先争取清晰和正确,并且只在需要时才使程序有效。

代码语言:javascript
复制
-module(tut7).
-export([format_temps/1]).

format_temps(List_of_cities) ->
    Converted_List = convert_list_to_c(List_of_cities),
    print_temp(Converted_List),
    {Max_city, Min_city} = find_max_and_min(Converted_List),
    print_max_and_min(Max_city, Min_city).

convert_list_to_c([{Name, {f, Temp}} | Rest]) ->
    Converted_City = {Name, {c, (Temp -32)* 5 / 9}},
    [Converted_City | convert_list_to_c(Rest)];

convert_list_to_c([City | Rest]) ->
    [City | convert_list_to_c(Rest)];

convert_list_to_c([]) ->
    [].

print_temp([{Name, {c, Temp}} | Rest]) ->
    io:format("~-15w ~w c~n", [Name, Temp]),
    print_temp(Rest);
print_temp([]) ->
    ok.

find_max_and_min([City | Rest]) ->
    find_max_and_min(Rest, City, City).

find_max_and_min([{Name, {c, Temp}} | Rest], 
         {Max_Name, {c, Max_Temp}}, 
         {Min_Name, {c, Min_Temp}}) ->
    if 
        Temp > Max_Temp ->
            Max_City = {Name, {c, Temp}};           % Change
        true -> 
            Max_City = {Max_Name, {c, Max_Temp}} % Unchanged
    end,
    if
         Temp < Min_Temp ->
            Min_City = {Name, {c, Temp}};           % Change
        true -> 
            Min_City = {Min_Name, {c, Min_Temp}} % Unchanged
    end,
    find_max_and_min(Rest, Max_City, Min_City);

find_max_and_min([], Max_City, Min_City) ->
    {Max_City, Min_City}.

print_max_and_min({Max_name, {c, Max_temp}}, {Min_name, {c, Min_temp}}) ->
    io:format("Max temperature was ~w c in ~w~n", [Max_temp, Max_name]),
    io:format("Min temperature was ~w c in ~w~n", [Min_temp, Min_name]).
代码语言:javascript
复制
58> c(tut7).
{ok, tut7}
59> tut7:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          -10 c
cape_town       21.11111111111111 c
stockholm       -4 c
paris           -2.2222222222222223 c
london          2.2222222222222223 c
Max temperature was 21.11111111111111 c in cape_town
Min temperature was -10 c in moscow
ok

2.12 If和案例

函数find_max_and_min可以计算出最高温度和最低温度。if这里介绍一个新的构造。如果工作如下:

代码语言:javascript
复制
if
    Condition 1 ->
        Action 1;
    Condition 2 ->
        Action 2;
    Condition 3 ->
        Action 3;
    Condition 4 ->
        Action 4
end

请注意,没有“;” 之前end。条件与守卫一样,即测试成功或失败。Erlang从顶部开始并进行测试,直至找到成功的条件。然后评估(执行)该条件后面的操作,并忽略所有其他条件和操作end。如果没有条件匹配,则会发生运行时故障。总是成功的条件是原子true。这通常用于最后一个if,意思是,true如果所有其他条件都失败,则执行后面的操作。

以下是一个简短的程序来显示工作if

代码语言:javascript
复制
-module(tut9).
-export([test_if/2]).

test_if(A, B) ->
    if 
        A == 5 ->
            io:format("A == 5~n", []),
            a_equals_5;
        B == 6 ->
            io:format("B == 6~n", []),
            b_equals_6;
        A == 2, B == 3 ->                      %That is A equals 2 and B equals 3
            io:format("A == 2, B == 3~n", []),
            a_equals_2_b_equals_3;
        A == 1 ; B == 7 ->                     %That is A equals 1 or B equals 7
            io:format("A == 1 ; B == 7~n", []),
            a_equals_1_or_b_equals_7
    end.

测试此程序提供:

代码语言:javascript
复制
60> c(tut9).
{ok,tut9}
61> tut9:test_if(5,33).
A == 5
a_equals_5
62> tut9:test_if(33,6).
B == 6
b_equals_6
63> tut9:test_if(2, 3).
A == 2, B == 3
a_equals_2_b_equals_3
64> tut9:test_if(1, 33).
A == 1 ; B == 7
a_equals_1_or_b_equals_7
65> tut9:test_if(33, 7).
A == 1 ; B == 7
a_equals_1_or_b_equals_7
66> tut9:test_if(33, 33).
** exception error: no true branch found when evaluating an if expression
     in function  tut9:test_if/2 (tut9.erl, line 5)

注意tut9:test_if(33,33)不会导致任何条件成功。这导致运行时错误if_clause,在这里很好地被shell格式化。查看Guard Sequences可用的许多警卫测试的详细信息。

case是Erlang中的另一个构造。回想一下这个convert_length函数的写法是:

代码语言:javascript
复制
convert_length({centimeter, X}) ->
    {inch, X / 2.54};
convert_length({inch, Y}) ->
    {centimeter, Y * 2.54}.

同样的程序也可以写成:

代码语言:javascript
复制
-module(tut10).
-export([convert_length/1]).

convert_length(Length) ->
    case Length of
        {centimeter, X} ->
            {inch, X / 2.54};
        {inch, Y} ->
            {centimeter, Y * 2.54}
    end.
代码语言:javascript
复制
67> c(tut10).
{ok,tut10}
68> tut10:convert_length({inch, 6}).
{centimeter,15.24}
69> tut10:convert_length({centimeter, 2.5}).
{inch,0.984251968503937}

二者caseif具有返回值,也就是,在上述例子中case返回的任一{inch,X/2.54}{centimeter,Y*2.54}。行为case也可以通过使用警卫来修改。以下示例阐明了这一点。它告诉我们一年的时间长短。今年一定是人所共知的,因为二月份的闰年有29天。

代码语言:javascript
复制
-module(tut11).
-export([month_length/2]).

month_length(Year, Month) ->
    %% All years divisible by 400 are leap
    %% Years divisible by 100 are not leap (except the 400 rule above)
    %% Years divisible by 4 are leap (except the 100 rule above)
    Leap = if
        trunc(Year / 400) * 400 == Year ->
            leap;
        trunc(Year / 100) * 100 == Year ->
            not_leap;
        trunc(Year / 4) * 4 == Year ->
            leap;
        true ->
            not_leap
    end,  
    case Month of
        sep -> 30;
        apr -> 30;
        jun -> 30;
        nov -> 30;
        feb when Leap == leap -> 29;
        feb -> 28;
        jan -> 31;
        mar -> 31;
        may -> 31;
        jul -> 31;
        aug -> 31;
        oct -> 31;
        dec -> 31
    end.
代码语言:javascript
复制
70> c(tut11).
{ok,tut11}
71> tut11:month_length(2004, feb).
29
72> tut11:month_length(2003, feb).
28
73> tut11:month_length(1947, aug).
31

2.13内置函数(BIF)

BIF是由于某种原因内置于Erlang虚拟机的功能。BIF经常实现不可能实现的功能或者效率太低而无法在Erlang中实现。一些BIF只能使用函数名称来调用,但它们默认属于erlang模块。例如,trunc以下对BIF 的呼叫相当于呼叫erlang:trunc

如图所示,首先检查一年是否飞跃。如果一年可以被400整除,这是一个闰年。要确定这一点,首先将年份除以400,然后使用BIF trunc(稍后再详细介绍)来切断任何小数。然后再乘以400,看看是否再次返回相同的值。例如,2004年:

代码语言:javascript
复制
2004 / 400 = 5.01
trunc(5.01) = 5
5 * 400 = 2000

2000年与2004年不一样,因此2004年不能被400整除。2000年:

代码语言:javascript
复制
2000 / 400 = 5.0
trunc(5.0) = 5
5 * 400 = 2000

那就是闰年。接下来的两个trunc测试评估年份是否可以通过100或4以相同的方式整除。第一个if返回leap或者not_leap,在变量中出现Leap。这个变量feb在下面的警卫中用于case告诉我们这个月有多长时间。

这个例子显示了使用trunc。使用Erlang运算符可以更轻松地rem进行分割后的余数,例如:

代码语言:javascript
复制
74> 2004 rem 400.
4

因此,与其写:

代码语言:javascript
复制
trunc(Year / 400) * 400 == Year ->
    leap;

它可以写成:

代码语言:javascript
复制
Year rem 400 == 0 ->
    leap;

还有许多其他的BIF如trunc。只有少数BIF可以用于警卫,并且不能使用自己定义在警卫中的功能。(参见Guard Sequences)(对于高级读者:这是为了确保守卫没有副作用。)让我们在shell中使用这些函数:

代码语言:javascript
复制
75> trunc(5.6).
5
76> round(5.6).
6
77> length([a,b,c,d]).
4
78> float(5).
5.0
79> is_atom(hello).
true
80> is_atom("hello").
false
81> is_tuple({paris, {c, 30}}).
true
82> is_tuple([paris, {c, 30}]).
false

所有这些都可以用于警卫。现在对于一些不能用于警卫的BIF:

代码语言:javascript
复制
83> atom_to_list(hello).
"hello"
84> list_to_atom("goodbye").
goodbye
85> integer_to_list(22).
"22"

这三个BIF做的转换在Erlang很难(或不可能)完成。

2.14高阶函数(Funs)

与大多数现代函数式编程语言一样,Erlang具有更高阶的函数。这里是一个使用shell的例子:

代码语言:javascript
复制
86> Xf = fun(X) -> X * 2 end.
#Fun<erl_eval.5.123085357>
87> Xf(5).
10

这里定义了一个函数,它将数字的值加倍并将该函数赋值给一个变量。因此Xf(5)返回值10.当使用列表时,有两个有用的函数是foreachmap,它们定义如下:

代码语言:javascript
复制
foreach(Fun, [First|Rest]) ->
    Fun(First),
    foreach(Fun, Rest);
foreach(Fun, []) ->
    ok.

map(Fun, [First|Rest]) -> 
    [Fun(First)|map(Fun,Rest)];
map(Fun, []) -> 
    [].

这两个功能在标准模块中提供listsforeach获取列表并将乐趣应用于列表中的每个元素。map通过对列表中的每个元素应用乐趣来创建新列表。回到shell中,map使用了一个有趣的功能来为列表的每个元素添加3:

代码语言:javascript
复制
88> Add_3 = fun(X) -> X + 3 end.
#Fun<erl_eval.5.123085357>
89> lists:map(Add_3, [1,2,3]).
[4,5,6]

让我们(再次)在城市列表中打印温度:

代码语言:javascript
复制
90> Print_City = fun({City, {X, Temp}}) -> io:format("~-15w ~w ~w~n",
[City, X, Temp]) end.
#Fun<erl_eval.5.123085357>
91> lists:foreach(Print_City, [{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          c -10
cape_town       f 70
stockholm       c -4
paris           f 28
london          f 36
ok

现在让我们定义一个可以用来浏览城市和温度列表并将它们全部转换为摄氏温度的乐趣。

代码语言:javascript
复制
-module(tut13).

-export([convert_list_to_c/1]).

convert_to_c({Name, {f, Temp}}) ->
    {Name, {c, trunc((Temp - 32) * 5 / 9)}};
convert_to_c({Name, {c, Temp}}) ->
    {Name, {c, Temp}}.

convert_list_to_c(List) ->
    lists:map(fun convert_to_c/1, List).
代码语言:javascript
复制
92> tut13:convert_list_to_c([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
[{moscow,{c,-10}},
 {cape_town,{c,21}},
 {stockholm,{c,-4}},
 {paris,{c,-2}},
 {london,{c,2}}]

convert_to_c功能与以前相同,但在这里它被用作一种乐趣:

代码语言:javascript
复制
lists:map(fun convert_to_c/1, List)

当其他地方定义的函数被用作fun时,它可以被称为Function/Arity(记住Arity=参数的数量)。所以在map-call lists:map(fun convert_to_c/1, List)中写入。如图所示,convert_list_to_c变得更短,更容易理解。

标准模块lists还包含一个函数sort(Fun, List),其中包含Fun两个参数。true如果第一个参数小于第二个参数,则返回该乐趣,否则返回false。排序添加到convert_list_to_c

代码语言:javascript
复制
-module(tut13).

-export([convert_list_to_c/1]).

convert_to_c({Name, {f, Temp}}) ->
    {Name, {c, trunc((Temp - 32) * 5 / 9)}};
convert_to_c({Name, {c, Temp}}) ->
    {Name, {c, Temp}}.

convert_list_to_c(List) ->
    New_list = lists:map(fun convert_to_c/1, List),
    lists:sort(fun({_, {c, Temp1}}, {_, {c, Temp2}}) ->
                       Temp1 < Temp2 end, New_list).
代码语言:javascript
复制
93> c(tut13).
{ok,tut13}
94> tut13:convert_list_to_c([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
[{moscow,{c,-10}},
 {stockholm,{c,-4}},
 {paris,{c,-2}},
 {london,{c,2}},
 {cape_town,{c,21}}]

sort乐趣被用来:

代码语言:javascript
复制
fun({_, {c, Temp1}}, {_, {c, Temp2}}) -> Temp1 < Temp2 end,

这里引入了一个匿名变量 “_” 的概念。这只是获取值的变量的简写,但该值将被忽略。这可以在任何适合的地方使用,而不仅仅是在乐趣中。如果小于,则Temp1 < Temp2返回。trueTemp1Temp2

扫码关注腾讯云开发者

领取腾讯云代金券

http://www.vxiaotou.com