2 顺序编程
2.1 Erlang Shell
大多数操作系统都有一个命令解释器或 shell,UNIX 和 Linux 有很多,Windows 有命令提示符。Erlang 有自己的 shell,可以在其中直接编写 Erlang 代码段,并进行评估以查看结果(参见 STDLIB 中的 shell(3) 手册页)。
通过在您的操作系统中启动一个 shell 或命令解释器并键入 erl 来启动 Erlang shell(在 Linux 或 UNIX 中)。您将看到类似以下内容。
% 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>
在 shell 中键入“2 + 5.”,然后按 Enter(回车键)。请注意,您通过以句点“.”和回车键结束来告诉 shell 您已完成代码输入。
1> 2 + 5.
7
2>
如所示,Erlang shell 对可以输入的行进行编号(如 1> 2>),并且它正确地表示 2 + 5 等于 7。如果您在 shell 中输入错误,可以使用退格键进行删除,就像在大多数 shell 中一样。shell 中还有更多编辑命令(参见 ERTS 用户指南中的 tty - 命令行界面)。
(请注意,以下示例中 shell 给出的许多行号是不连续的。这是因为本教程是在单独的会话中编写和代码测试的)。
以下是一个更复杂的计算
2> (42 + 77) * 66 / 3.
2618.0
请注意方括号、乘法运算符“*”和除法运算符“/”的使用,就像在普通算术中一样(参见 表达式)。
按 Ctrl-C 关闭 Erlang 系统和 Erlang shell。
将显示以下输出
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()
3> halt().
%
2.2 模块和函数
如果您只能从 shell 中运行代码,那么编程语言就没有多大用处。所以这里有一个小型的 Erlang 程序。使用合适的文本编辑器将其输入到名为 tut.erl 的文件中。文件名 tut.erl 非常重要,并且它也必须与您启动 erl 的目录相同)。如果您幸运的话,您的编辑器有 Erlang 模式,可以让您更轻松地输入和格式化代码(参见工具用户指南中的 Emacs 的 Erlang 模式),但没有它您也可以完全正常地操作。以下是需要输入的代码
-module(tut). -export([double/1]). double(X) -> 2 * X.
不难猜到,该程序将数字的值加倍。代码的前两行将在后面介绍。让我们编译该程序。这可以在 Erlang shell 中完成,如下所示,其中 c 表示编译
3> c(tut).
{ok,tut}
{ok,tut} 表示编译成功。如果显示“error”,则表示您输入的文本中存在错误。附加的错误消息会提示错误所在,以便您可以修改文本,然后再次尝试编译程序。
现在运行该程序
4> tut:double(10).
20
正如预期的那样,10 的两倍是 20。
现在让我们回到代码的前两行。Erlang 程序被写入文件。每个文件都包含一个 Erlang 模块。模块中的第一行代码是模块名称(参见 模块)
-module(tut).
因此,该模块称为 tut。请注意行末的句点“.”。用于存储模块的文件必须与模块具有相同的名称,但扩展名为“.erl”。在本例中,文件名是 tut.erl。在使用另一个模块中的函数时,使用语法 module_name:function_name(arguments)。因此,以下表示调用模块 tut 中的函数 double,并带有一个参数“10”。
4> tut:double(10).
第二行表示模块 tut 包含一个名为 double 的函数,该函数接受一个参数(在本例中为 X)
-export([double/1]).
第二行还表示可以从模块 tut 之外调用此函数。稍后将详细介绍。同样,请注意行末的“.”。
现在来介绍一个更复杂的示例,即一个数字的阶乘。例如,4 的阶乘是 4 * 3 * 2 * 1,等于 24。
将以下代码输入到名为 tut1.erl 的文件中
-module(tut1). -export([fac/1]). fac(1) -> 1; fac(N) -> N * fac(N - 1).
所以这是一个名为 tut1 的模块,它包含一个名为 fac> 的函数,该函数接受一个参数 N。
第一部分表示 1 的阶乘为 1。
fac(1) -> 1;
请注意,这部分以分号“;”结尾,表示函数 fac> 还有更多内容。
第二部分表示 N 的阶乘等于 N 乘以 N - 1 的阶乘
fac(N) -> N * fac(N - 1).
请注意,这部分以句点“.”结尾,表示该函数没有更多部分。
编译该文件
5> c(tut1).
{ok,tut1}
现在计算 4 的阶乘。
6> tut1:fac(4).
24
这里调用了模块 tut1 中的函数 fac>,参数为 4。
一个函数可以有多个参数。让我们用一个函数来扩展模块 tut1,该函数用来乘以两个数字
-module(tut1). -export([fac/1, mult/2]). fac(1) -> 1; fac(N) -> N * fac(N - 1). mult(X, Y) -> X * Y.
请注意,还需要扩展 -export 行,以包含有关另一个函数 mult 的信息,该函数有两个参数。
编译
7> c(tut1).
{ok,tut1}
试用新的函数 mult
8> tut1:mult(3,4).
12
在本例中,这些数字是整数,代码中的函数参数 N、X 和 Y 被称为变量。变量必须以大写字母开头(参见 变量)。变量示例包括 Number、ShoeSize 和 Age。
2.3 原子
原子是 Erlang 中的另一种数据类型。原子以小写字母开头(参见 原子),例如,charles、centimeter 和 inch。原子只是名称,仅此而已。它们不像变量,变量可以有值。
将下一个程序输入到名为 tut2.erl 的文件中)。它可以用于在英寸和厘米之间进行转换
-module(tut2). -export([convert/2]). convert(M, inch) -> M / 2.54; convert(N, centimeter) -> N * 2.54.
编译
9> c(tut2).
{ok,tut2}
测试
10> tut2:convert(3, inch). 1.1811023622047243 11> tut2:convert(7, centimeter). 17.78
请注意,这里引入了小数(浮点数),但没有进行解释。希望您能理解这一点。
让我们看看如果在 convert 函数中输入了除 centimeter 或 inch 之外的其他内容,会发生什么
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 输出
13> v(12).
{'EXIT',{function_clause,[{tut2,convert,
[3,miles],
[{file,"tut2.erl"},{line,4}]},
{erl_eval,do_apply,6,
[{file,"erl_eval.erl"},{line,677}]},
{shell,exprs,7,[{file,"shell.erl"},{line,687}]},
{shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
{shell,eval_loop,3,
[{file,"shell.erl"},{line,627}]}]}}
2.4 元组
现在,tut2 程序的编程风格不太好。考虑一下
tut2:convert(3, inch).
这是否意味着 3 是以英寸为单位?或者它是否意味着 3 是以厘米为单位,并且需要转换为英寸?Erlang 有一种方法可以将事物组合在一起,使其更易于理解。这些组合称为元组,并用花括号“{”和“}”括起来。
因此,{inch,3} 表示 3 英寸,{centimeter,5} 表示 5 厘米。现在让我们编写一个新的程序,用于将厘米转换为英寸,反之亦然。将以下代码输入到名为 tut3.erl 的文件中)
-module(tut3). -export([convert_length/1]). convert_length({centimeter, X}) -> {inch, X / 2.54}; convert_length({inch, Y}) -> {centimeter, Y * 2.54}.
编译和测试
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 行(上面)是如何工作的。传递给函数 convert_length 的参数 {inch,5} 首先与 convert_length 的第一个头部子句匹配,即 convert_length({centimeter,X})。可以看出,{centimeter,X} 与 {inch,5} 不匹配(头部是“->”之前的部分)。由于匹配失败,让我们尝试下一个子句的头部,即 convert_length({inch,Y})。这匹配成功,Y 获得了值 5。
元组可以包含两个以上的部分,实际上可以包含任意数量的部分,并且可以包含任何有效的 Erlang 项。例如,要表示世界各地不同城市的温度
{moscow, {c, -10}} {cape_town, {f, 70}} {paris, {f, 28}}
元组中的项数是固定的。元组中的每个项都称为元素。在元组 {moscow,{c,-10}} 中,元素 1 是 moscow,元素 2 是 {c,-10}。这里 c 代表摄氏度,f 代表华氏度。
2.5 列表
元组可以将事物组合在一起,但还需要表示事物的列表。Erlang 中的列表用方括号“["和"]”括起来。例如,世界各地不同城市的温度列表可以为
[{moscow, {c, -10}}, {cape_town, {f, 70}}, {stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]
请注意,此列表很长,无法在一行中显示。这没关系,Erlang 允许在所有“合理的位置”换行,但不允许在原子、整数和其他内容的中间换行。
查看列表部分的一个有用方法是使用“|”。这最好用 shell 中的示例来解释
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]。
另一个示例
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]
这里您看到了使用 | 从列表中获取前两个元素。如果您尝试从列表中获取比列表中的元素更多的元素,则会返回错误。请注意,列表没有元素的特殊情况,即 []
24> [A, B | C] = [1, 2]. [1,2] 25> A. 1 26> B. 2 27> C. []
在前面的示例中,使用了新的变量名,而不是重复使用旧的变量名:First、TheRest、E1、E2、R、A、B 和 C。这样做的原因是,变量在其上下文(作用域)中只能赋值一次。稍后将详细介绍。
以下示例展示了如何查找列表的长度。将以下代码输入到名为 tut4.erl 的文件中)
-module(tut4). -export([list_length/1]). list_length([]) -> 0; list_length([First | Rest]) -> 1 + list_length(Rest).
编译和测试
28> c(tut4). {ok,tut4} 29> tut4:list_length([1,2,3,4,5,6,7]). 7
解释
list_length([]) -> 0;
空列表的长度显然是 0。
list_length([First | Rest]) -> 1 + list_length(Rest).
包含第一个元素 First 和剩余元素 Rest 的列表的长度为 1 加上 Rest 的长度。
(仅供高级读者参考:这不是尾递归,有更好的方法来编写此函数。)
通常,元组用于其他语言中使用“记录”或“结构体”的地方。另外,列表用于表示大小可变的事物,即在其他语言中使用链表的地方。
Erlang 没有字符串数据类型。相反,字符串可以通过 Unicode 字符列表来表示。例如,这意味着列表 [97,98,99] 等效于 "abc"。Erlang shell 很“聪明”,它会猜测你指的是哪个列表,并以它认为最合适的形式输出它,例如
30> [97,98,99].
"abc"
2.6 地图
地图是一组键值关联。这些关联用 "#{" 和 "}" 封装起来。要创建从 "key" 到值 42 的关联
> #{ "key" => 42 }. #{"key" => 42}
让我们直接从使用一些有趣功能的示例开始。
以下示例展示了如何使用地图引用颜色和 alpha 通道来计算 alpha 混合。将代码输入名为 color.erl 的文件中。
-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).
编译和测试
> 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}
此示例需要一些解释
-define(is_channel(V), (is_float(V) andalso V >= 0.0 andalso V =< 1.0)).
首先定义一个宏 is_channel 来帮助进行守卫测试。这仅仅是为了方便,并减少语法混乱。有关宏的更多信息,请参阅 预处理器。
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 创建一个新的地图项,并让键 red、green、blue 和 alpha 与初始值关联。在这种情况下,只允许介于 0.0 和 1.0 之间(包括 0.0 和 1.0)的浮点值,如每个参数的 ?is_channel/1 宏所确保的。创建新地图时,只允许 => 运算符。
通过调用 blend/2 对 new/4 创建的任何颜色项,可以根据两个地图项计算出结果颜色。
blend/2 的第一步是计算结果 alpha 通道
alpha(#{alpha := SA}, #{alpha := DA}) -> SA + DA*(1.0 - SA).
使用 := 运算符获取与键 alpha 关联的值,用于两个参数。地图中的其他键将被忽略,只需要键 alpha 并对其进行检查。
函数 red/2、blue/2 和 green/2 也一样。
red(#{red := SV, alpha := SA}, #{red := DV, alpha := DA}) -> SV*SA + DV*DA*(1.0 - SA).
不同之处在于,它会检查每个地图参数中的两个键。其他键将被忽略。
最后,让我们在 blend/3 中返回结果颜色
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 包含许多有助于进行格式化输入/输出的函数。要查找有关标准模块的信息,可以在操作系统 shell 或命令提示符(启动 erl 的地方)中使用命令 erl -man。尝试以下操作系统 shell 命令
% 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-
...
如果你的系统无法运行,则文档包含在 Erlang/OTP 版本中,以 HTML 格式提供。你也可以从 www.erlang.se(商业 Erlang)或 www.erlang.org(开源)网站上以 HTML 格式阅读文档或以 PDF 格式下载文档。例如,对于 Erlang/OTP 版本 R9B
https://erlang.ac.cn/doc/r9b/doc/index.html
2.8 将输出写入终端
能够在示例中进行格式化输出是件好事,因此下一个示例展示了如何使用 io:format 函数的简单方法。与所有其他导出函数一样,你可以在 shell 中测试 io:format 函数
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 一个更大的示例
现在让我们来看一个更大的示例,来巩固你到目前为止学到的知识。假设你有一个来自世界各地多个城市的温度读数列表。其中一些以摄氏度表示,一些以华氏度表示(如前面的列表)。首先让我们将它们全部转换为摄氏度,然后让我们整齐地打印数据。
%% 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]).
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 的字段长度(宽度)打印“项”,并向左对齐。(参见 io(3))STDLIB 中的手册页。
现在用列表的其余部分作为参数调用 format_temps(Rest)。这种做法类似于其他语言中的循环结构。(是的,这是递归,但不要担心。)因此,再次调用相同的 format_temps 函数,这一次 City 获取值 {cape_town,{f,70}},并重复之前的过程。重复此过程,直到列表变为空,即 [],这会导致第一个子句 format_temps([]) 匹配。这只是简单地返回(结果是)原子 ok,因此程序结束。
2.10 匹配、守卫和变量的作用域
在这样的列表中找到最大温度和最小温度可能很有用。在扩展程序以执行此操作之前,让我们看看用于查找列表中元素的最大值的函数
-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).
37> c(tut6). {ok,tut6} 38> tut6:list_max([1,2,3,4,5,7,4,3,2,1]). 7
首先请注意,两个函数具有相同的名称,即 list_max。但是,它们每个都接受不同数量的参数(参数)。在 Erlang 中,它们被认为是完全不同的函数。如果你需要区分这些函数,请编写 Name/Arity,其中 Name 是函数名称,Arity 是参数数量,在本例中是 list_max/1 和 list_max/2。
在这个示例中,你遍历一个列表,并“携带”一个值,在本例中是 Result_so_far。list_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_far 时,使用 Head 而不是 Result_so_far。when 是一个特殊词,用于在函数中的 -> 之前,用于表示只有在后面的测试为真时,才使用函数的这一部分。此类测试称为 **守卫**。如果守卫为假(即守卫失败),则尝试函数的下一部分。在本例中,如果 Head 不大于 Result_so_far,那么它必须小于或等于它。这意味着不需要在函数的下一部分上设置守卫。
守卫中一些有用的运算符是
- < 小于
- > 大于
- == 等于
- >= 大于或等于
- =< 小于或等于
- /= 不等于
(参见 守卫序列)。
要将上面的程序更改为一个计算列表中元素的最小值的程序,你只需要编写 < 而不是 >。(但最好将函数名称更改为 list_min。)
前面提到过,一个变量在其作用域内只能赋值一次。在上面的示例中,你看到 Result_so_far 被赋值了几次。这是可以的,因为每次调用 list_max/2 时,你都会创建一个新的作用域,可以将 Result_so_far 视为每个作用域中的一个不同的变量。
另一种创建和赋值变量的方法是使用匹配运算符 =。因此,如果你编写 M = 5,将创建一个名为 M 的变量,其值为 5。如果在同一个作用域中,你随后编写 M = 6,则会返回错误。在 shell 中尝试一下
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
匹配运算符的使用对于拆分 Erlang 项和创建新项特别有用。
43> {X, Y} = {paris, {f, 28}}. {paris,{f,28}} 44> X. paris 45> Y. {f,28}
这里 X 获取值 paris,而 Y{f,28}。
如果你尝试使用另一个城市再次执行相同的操作,则会返回错误
46> {X, Y} = {london, {f, 36}}.
** exception error: no match of right hand side value {london,{f,36}}
变量还可以用于提高程序的可读性。例如,在上面的函数 list_max/2 中,你可以编写
list_max([Head|Rest], Result_so_far) when Head > Result_so_far -> New_result_far = Head, list_max(Rest, New_result_far);
这可能更清晰一些。
2.11 关于列表的更多信息
请记住,可以使用 | 运算符获取列表的头部
47> [M1|T1] = [paris, london, rome]. [paris,london,rome] 48> M1. paris 49> T1. [london,rome]
| 操作符也可以用于在列表前面添加一个头部。
50> L1 = [madrid | T1]. [madrid,london,rome] 51> L1. [madrid,london,rome]
现在举一个在处理列表时的例子——反转列表的顺序。
-module(tut8). -export([reverse/1]). reverse(List) -> reverse(List, []). reverse([Head | Rest], Reversed_List) -> reverse(Rest, [Head | Reversed_List]); reverse([], Reversed_List) -> Reversed_List.
52> c(tut8). {ok,tut8} 53> tut8:reverse([1,2,3]). [3,2,1]
考虑 Reversed_List 是如何构建的。它最初为空列表 `[]`,然后依次从要反转的列表中取出头部并添加到 Reversed_List 中,如下所示。
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 包含许多用于操作列表的函数,例如用于反转列表的函数。所以在编写列表操作函数之前,最好先检查一下是否已经为你编写了这样的函数(请参阅 STDLIB 中的 lists(3) 手册页)。
现在让我们回到城市和温度,但这次采用更结构化的方式。首先,我们将整个列表转换为摄氏度,如下所示。
-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([]) -> [].
测试函数。
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}}]
解释
format_temps(List_of_cities) -> convert_list_to_c(List_of_cities).
这里 format_temps/1 调用了 convert_list_to_c/1。 convert_list_to_c/1 从 List_of_cities 中取出头部,如果需要将其转换为摄氏度。| 操作符用于将(可能已经)转换后的头部添加到已转换的剩余列表中。
[Converted_City | convert_list_to_c(Rest)];
或者
[City | convert_list_to_c(Rest)];
这将一直进行,直到列表的结尾,即列表为空。
convert_list_to_c([]) -> [].
现在,当列表转换完成后,添加一个函数来打印它。
-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.
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
现在,必须添加一个函数来查找具有最高温度和最低温度的城市。下面的程序不是执行此操作最有效的方式,因为您需要遍历城市列表四次。但是,最好首先追求清晰度和正确性,只有在需要时才提高程序效率。
-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]).
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 和 Case
函数 find_max_and_min 计算出最高温度和最低温度。这里引入了新的结构 if。 If 的工作方式如下。
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 的工作原理。
-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.
测试该程序将得到以下结果。
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 友好地格式化。有关可用守卫测试的详细信息,请参见 守卫序列。
case 是 Erlang 中的另一个结构。回想一下,convert_length 函数的写法是。
convert_length({centimeter, X}) -> {inch, X / 2.54}; convert_length({inch, Y}) -> {centimeter, Y * 2.54}.
同一个程序也可以写成这样。
-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.
67> c(tut10). {ok,tut10} 68> tut10:convert_length({inch, 6}). {centimeter,15.24} 69> tut10:convert_length({centimeter, 2.5}). {inch,0.984251968503937}
case 和 if 都有 **返回值**,即在上面的例子中,case 返回 {inch,X/2.54} 或 {centimeter,Y*2.54}。 case 的行为也可以通过使用守卫来修改。以下示例对此进行了说明。它告诉我们一个月的长度,给定年份。必须知道年份,因为闰年的 2 月有 29 天。
-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.
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 内置函数 (BIFs)
BIFs 是由于某种原因而内置到 Erlang 虚拟机中的函数。BIFs 通常实现 Erlang 中不可能或效率太低才能实现的功能。一些 BIFs 可以只使用函数名调用,但默认情况下它们属于 erlang 模块。例如,下面对 BIF trunc 的调用等效于对 erlang:trunc 的调用。
如所示,首先检查年份是否是闰年。如果年份可以被 400 整除,那么它就是闰年。为了确定这一点,首先将年份除以 400,然后使用 BIF trunc(稍后将详细介绍)来截断所有小数。然后再次乘以 400,看看是否返回了相同的值。例如,年份 2004
2004 / 400 = 5.01 trunc(5.01) = 5 5 * 400 = 2000
2000 与 2004 不相同,因此 2004 不能被 400 整除。年份 2000
2000 / 400 = 5.0 trunc(5.0) = 5 5 * 400 = 2000
也就是说,闰年。接下来的两个 trunc 测试以相同的方式评估年份是否可以被 100 或 4 整除。第一个 if 返回 leap 或 not_leap,它们最终存储在变量 Leap 中。该变量用于以下 case 的守卫中,该守卫告诉我们月份的长度。
此示例演示了 trunc 的用法。使用 Erlang 运算符 rem 更容易,它返回除法后的余数,例如
74> 2004 rem 400.
4
所以,与其写
trunc(Year / 400) * 400 == Year -> leap;
不如写成这样
Year rem 400 == 0 -> leap;
还有许多其他 BIFs,例如 trunc。只有少数 BIFs 可用于守卫,您不能在守卫中使用自己定义的函数。(请参阅 守卫序列)(对于高级读者:这是为了确保守卫没有副作用。)让我们在 shell 中玩一玩这些函数。
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
所有这些函数都可以在守卫中使用。现在,让我们来看一些不能在守卫中使用的 BIFs。
83> atom_to_list(hello). "hello" 84> list_to_atom("goodbye"). goodbye 85> integer_to_list(22). "22"
这三个 BIFs 执行了在 Erlang 中很难(或不可能)执行的转换。
2.14 高阶函数 (Funs)
与大多数现代函数式编程语言一样,Erlang 也具有高阶函数。以下是一个使用 shell 的示例。
86> Xf = fun(X) -> X * 2 end. #Fun<erl_eval.5.123085357> 87> Xf(5). 10
这里定义了一个将数字值加倍的函数,并将该函数分配给一个变量。因此,Xf(5) 返回值 10。在处理列表时,有两个有用的函数是 foreach 和 map,它们定义如下。
foreach(Fun, [First|Rest]) -> Fun(First), foreach(Fun, Rest); foreach(Fun, []) -> ok. map(Fun, [First|Rest]) -> [Fun(First)|map(Fun,Rest)]; map(Fun, []) -> [].
这两个函数在标准模块 lists 中提供。 foreach 接受一个列表,并对列表中的每个元素应用一个 fun。 map 通过对列表中的每个元素应用一个 fun 来创建一个新列表。回到 shell,使用 map 和一个 fun 为列表的每个元素添加 3
88> Add_3 = fun(X) -> X + 3 end. #Fun<erl_eval.5.123085357> 89> lists:map(Add_3, [1,2,3]). [4,5,6]
让我们(再次)打印城市列表中的温度。
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
现在让我们定义一个 fun,它可以用于遍历城市和温度列表,并将它们全部转换为摄氏度。
-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).
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 与以前相同,但这里它用作一个 fun
lists:map(fun convert_to_c/1, List)
当在其他地方定义的函数用作 fun 时,它可以被称为 Function/Arity(记住 Arity = 参数数量)。因此,在 map 调用中,写的是 lists:map(fun convert_to_c/1, List)。如所示,convert_list_to_c 变得更短,更容易理解。
标准模块 lists 还包含一个函数 sort(Fun, List),其中 Fun 是一个具有两个参数的 fun。如果第一个参数小于第二个参数,则该 fun 返回 true,否则返回 false。将排序添加到 convert_list_to_c 中
-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).
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 中,fun 的用法如下。
fun({_, {c, Temp1}}, {_, {c, Temp2}}) -> Temp1 < Temp2 end,
这里引入了 **匿名变量** "_" 的概念。这只是用于接收值的变量的简写,但该值会被忽略。它可以在任何合适的地方使用,而不仅仅是在 fun 中。Temp1 < Temp2 如果 Temp1 小于 Temp2,则返回 true。