查看源码 顺序编程
Erlang Shell
大多数操作系统都有一个命令解释器或 shell,UNIX 和 Linux 有很多,Windows 有命令提示符、powershell 等。Erlang 有自己的 shell,可以直接编写 Erlang 代码片段,并进行求值以查看结果(请参阅 STDLIB 中的 shell
手册页)。
通过在您的操作系统中启动一个 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
请注意括号、乘法运算符 *
和除法运算符 /
的使用,这与正常的算术运算相同(请参阅 表达式)。
按 Control-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/0
3> halt().
$
模块和函数
如果只能从 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
此处,使用参数 4
调用模块 tut1
中的函数 fac
。
一个函数可以有多个参数。让我们使用将两个数字相乘的函数扩展模块 tut1
-module(tut1).
-export([fac/1, mult/2]).
fac(1) ->
1;
fac(N) ->
N * fac(N - 1).
mult(X, Y) ->
X * Y.
请注意,还需要使用有关另一个具有两个参数的函数 mult
的信息来扩展 -export
行。
编译
7> c(tut1).
{ok,tut1}
尝试新函数 mult
8> tut1:mult(3,4).
12
在此示例中,数字是整数,代码中函数中的参数 N
、X
和 Y
称为变量。变量必须以大写字母开头(请参阅 变量)。变量的示例包括 Number
、ShoeSize
和 Age
。
原子
原子是 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}]}]}}
元组
现在 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 行(上文)是如何工作的。给函数 {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
表示华氏度。
列表
元组将事物组合在一起,还需要表示事物列表。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"
映射
映射是一组键值关联。这些关联用 #{
和 }
封装。要创建从 "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
宏确保。创建新映射时,只允许使用 =>
运算符。
通过对由 new/4
创建的任何颜色项调用 blend/2
,可以计算出由两个映射项确定的结果颜色。
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
映射会使用新的通道值进行更新。使用 :=
运算符更新现有键的新值。
标准模块和手册页
Erlang 有许多标准模块可以帮助您完成各种操作。例如,模块 io
包含许多有助于进行格式化输入/输出的函数。要查找有关标准模块的信息,可以在 erlang shell 中使用命令 h(..)
。试试 erlang shell 命令
1> h(io).
io
Standard I/O server interface functions.
This module provides an interface to standard Erlang I/O servers. The output
functions all return `ok` if they are successful, or exit if they are not.
...
如果这在您的系统上不起作用,则文档将作为 HTML 包含在 Erlang/OTP 版本中。您还可以从 <www.erlang.org/doc> 中以 HTML 格式阅读文档或将其下载为 epub 格式。
将输出写入终端
在示例中能够进行格式化输出是一件好事,因此下一个示例演示了使用 io:format/2
函数的简单方法。与所有其他导出的函数一样,您可以在 shell 中测试 io:format/2
函数
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
函数 io:format/2
(即,带有两个参数的 format
)接受两个列表。第一个列表几乎总是写在 " "
之间的列表。此列表会按原样打印出来,只是每个 ~w
都会被从第二个列表中按顺序获取的项替换。每个 ~n 都会被换行符替换。io:format/2
函数本身会在一切按计划进行时返回原子 ok
。与 Erlang 中的其他函数一样,如果发生错误,它会崩溃。这并非 Erlang 中的错误,而是一种经过深思熟虑的策略。Erlang 有复杂的机制来处理稍后将显示的错误。作为练习,尝试使 io:format/2
崩溃,这应该不难。但是请注意,尽管 io:format/2
崩溃,但 Erlang shell 本身不会崩溃。
更大的示例
现在有一个更大的示例来巩固您目前所学的知识。假设您有一个来自世界各地许多城市的温度读数列表。其中一些是以摄氏度为单位,一些是以华氏度为单位(如上一个列表所示)。首先,让我们将它们全部转换为摄氏度,然后让我们整齐地打印数据。
%% 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 的字段长度(宽度)打印“项”并将其左对齐。(请参阅 STDLIB 中的 io:fwrite/1
手册页)。
现在,使用列表的其余部分作为参数调用 format_temps(Rest)
。这种做事方式类似于其他语言中的循环结构。(是的,这是递归,但不要为此担心。)因此,再次调用相同的 format_temps
函数,这次 City
的值为 {cape_town,{f,70}}
,并且重复与之前相同的过程。此操作将一直执行,直到列表为空,即 [],这将导致第一个子句 format_temps([])
匹配。这只会返回(导致)原子 ok
,因此程序结束。
匹配、保护和变量作用域
在这样的列表中查找最大和最小温度可能很有用。在扩展程序执行此操作之前,让我们看一下查找列表中元素最大值的函数
-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);
这可能更清楚一些。
关于列表的更多信息
请记住,|
运算符可用于获取列表的头部
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
手册页)。
现在让我们回到城市和温度,但这次采用更结构化的方法。首先,让我们将整个列表转换为摄氏度,如下所示
-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
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
的行为也可以通过使用守卫来修改。以下示例对此进行了解释。它告诉我们给定年份的月份长度。年份必须已知,因为二月份在闰年有 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
内置函数 (BIF)
BIF 是由于某种原因内置到 Erlang 虚拟机中的函数。BIF 通常实现一些在 Erlang 中不可能或效率太低而无法实现的功能。某些 BIF 可以仅使用函数名称调用,但它们默认属于 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
中 feb
的守卫,该 case
告诉我们月份有多长。
此示例显示了 trunc
的用法。使用 Erlang 运算符 rem
更容易,该运算符给出除法后的余数,例如
74> 2004 rem 400.
4
因此,可以不用编写
trunc(Year / 400) * 400 == Year ->
leap;
可以编写为
Year rem 400 == 0 ->
leap;
还有许多其他 BIF,例如 trunc
。只有少数 BIF 可以用于守卫,并且您不能在守卫中使用自己定义的函数。(请参阅守卫序列)(对于高级读者:这是为了确保守卫没有副作用。)让我们在 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
所有这些都可以在守卫中使用。现在介绍一些不能在守卫中使用的 BIF
83> atom_to_list(hello).
"hello"
84> list_to_atom("goodbye").
goodbye
85> integer_to_list(22).
"22"
这三个 BIF 执行在 Erlang 中难以(或不可能)完成的转换。
高阶函数 (Fun)
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
。