SlideShare a Scribd company logo
Erlang 编程 (第一部分)
1 顺序编程
1.1 The Erlang Shell
大多数操作系统都有一个命令行交互环境或者一个 shell,对于 UNIX 和 LINUX 尤其是这样。Windows 也有
自己的命令行模式。Erlang 也同样有自己的 shell,我们可以直接在里面编写代码和运行我们编写的程序,
并在其中看到我们的输出情况。我们能够在多种操作系统上运行 Erlang 的 shell,一般来说在我们所使用的
操作系统的命令行中输入 erl 就可以了,我们将可能看到下面的提示:
% erl
Erlang (BEAM) emulator version 5.2 [source] [hipe]
Eshell V5.2 (abort with ^G)
1>
现在我们输入“2 + 5.”,(注意最后有一个英文句号,并不包括引号哈)
1> 2 + 5.
7
2>
在 Windows 中,我们还可以通过双击 Erlang shell 的图标来启动 Erlang shell。
你现在注意到了 Erlang shell 的命令行前面都有一个标号(例如 1> 2>),并且下面正确的输出了我们的答
案“7”!同样我们注意到我们的输入的最后有一个英文的句号,这是在我们最终按下回车之前的最后一个字
符。如果我们写在 shell 中的东西有什么错误,我们可以使用退格键进行删除,于是,我们推想我们可以使
用其他 shell 下的一些常用的编辑命令,而事实上确实是这样的,以后我们使用到的时候再介绍。
现在我们尝试一下稍微复杂一点的运算:
2> (42 + 77) * 66 / 3.
2618.00
我们现在使用了一些“复杂一点”的运算符号“*”和“/”,分别表示乘法运算和除法运算。除此之外还支持其他一
些运算符号,我们在以后使用到的时候再介绍。
我们打算关闭 Elrang 系统,则需要在 Erlang shell 中键入 Ctrl+C,我们将看到下面的输出:
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
(v)ersion (k)ill (D)b-tables (d)istribution
a
%
我们这时候键入“a”就会退出 Erlang 系统。
另一个方法就是键入 halt(),也可以实现退出 Erlang 系统的目的:
3> halt().
%
1.2 Modules 和 Functions (模块和函数)
一个编程语言如果只能让我们从 shell 中运行代码,那么可以说这个语言的用处受到了很大的限制,至少我
会感觉到不爽。
这里有一个小的 Erlang 程序。
我们将下面的内容键入一个叫做 tut.erl 的文件(这里需要注意
到的是我们的 tut.erl 文件应该放在 erl 程序的同一个目录下,文件名应该和模块名相同,这样 Erlang 才能
很好的找到我们的模块,至于编辑器随便一个支持纯文本的就可以哈)。如果我们的编辑器有一个 Erlang
模式就可能编写起来更加方便,并且代码的格式也会变得更加规范,但是我们也许也会产生对这些编辑器
或者 IDE 的强烈依赖性。下面就是我们要键入的代码:
-module(tut).
-export([double/1]).
double(X) ->
2 * X.
不难看出,我们的程序的任务就是将输入的数字进行“乘以 2”的操作。
我们后面会解释前两行的含义。
我们现
在来编译一下这个程序,我们在 Erlang shell 中键入下面的内容:
3> c(tut).
{ok,tut}
{ok,tut} 告诉我们编译成功的完成了。
如果显示的是“error”,那么可能我们的文件位置或者键入的代码文本存
在某些问题,出错信息会给我们一些提示,我们按照提示做就好了,仔细的检查代码在任何时候都是需要
的:)
现在我们来运行以下这个程序:
4> tut:double(10).
20
和预计的一样,10 的“乘以 2”之后是 20(貌似是废话)。
现在让我们回到最开始的两行代码。Erlang 程序一般是保存在文件中的。每个文件我们在 Erlang 中称为
module(模块)。第一行就是告诉我们这个模块的名字。
-module(tut).
这段代码告诉我们该模块的名称为“tut”。注意这行最后的“.”符号是必不可少的。这个模块名必须和保存这段
代码的文件(后缀为“erl”的文件)有相同的名称。所以这里我们要求了文件名必须是“tut.erl”的原因。当我们
在使用另一个模块中的函数时,我们使用下面的语法 module_name:function_name(arguments).所以:
4> tut:double(10).
意味着我们从 tut 模块中调用一个叫做 double 的函数,并且带有参数“10”.
第二行:
-export([double/1]).
含一位模块 tut 中包含一个名叫 double 的函数,并且带有一个参数(就是例子中的“10”),并且这个函数可
以被从模块 tut 之外调用到。更深入的内容我们后面会介绍。现在回头看看上面行尾的句号“.”。
现在有一个稍微复杂一点的例子,是用来进行阶乘(比如 4 的阶乘就是 1*2*3*4 的结果)操作的。键入下面
的代码到一个新建的文本文件,并命名为 tut1.erl:
-module(tut1).
-export([fac/1]).
fac(1) ->
1;
fac(N) ->
N * fac(N - 1).
编译这个文件:
5> c(tut1).
{ok,tut1}
现在我们计算 4 的阶乘:
6> tut1:fac(4).
24
第一部分:
fac(1) ->
1;
含义为 1 的阶乘是 1.注意到这部分最后的“;”符号,它预示着下面还有这个函数的其他部分。第二部分:
fac(N) ->
N * fac(N - 1).
含义是 N 的阶乘等于 N 乘以 N-1 的阶乘。
注意这部分最后的符号“.”,含义为对于该函数已经没有更多的内容
了,函数就此结束。
一个函数可以有很多的参数。让我们扩展这个 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}
试一下我们的代码是否正常工作了:
8> tut1:mult(3,4).
12
上面的例子使用了整数进行乘法运算,其中 N/X/Y 成为变量。
注意:变量必须是以大写字母开头,否则编译
会提示出错。下面的变量名就是合法的:Number,ShoeSize,Age 等。
1.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.18110
11> tut2:convert(7, centimeter).
17.7800
注意我们这里使用了十进制的数值(浮点类型),而并没有任何显式的的声明,但是我猜想你们是可以应
付这种情况的。
看一下我们键入其他东西会发生什么情况(除了 inch 和 centimeter 之外的):
13> tut2:convert(3, miles).
=ERROR REPORT==== 8-Oct-2006::22:52:46 ===
Error in process <0.25.0> with exit value: {function_clause,[{tut2,convert,
[3,miles]},{erl_eval,expr,3},{erl_eval,exprs,4},{shell,eval_loop,2}]}
** exited: {function_clause,[{tut2,convert,[3,miles]},
{erl_eval,expr,3},
{erl_eval,exprs,4},
{shell,eval_loop,2}]} **
这里有两部分被称为子句的内容存在于 convert 函数中,但是 miles 并不是这两部分中的其中一部分。于是
Erlang 系统不能成功的匹配函数中的子句调用,于是我们就看到了上面的出错提示 function_clause 消息。
上面的输出看上去是“典型的一团糟”,但是经过我们认真的观察,我们可以清楚的直到代码中到底是发生了
什么。
1.4 元组
现在的 tut2 程序并不具备一个很好的编程代码的风格。考虑下面的代码:
tut2:convert(3, inch).
意味着 3 的单位是 inches 英尺?还是 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}.
Compile and test:
14> c(tut3).
{ok,tut3}
15> tut3:convert_length({inch, 5}).
{centimeter,12.7000}
16> tut3:convert_length(tut3:convert_length({inch, 5})).
{inch,5.00000}
注意上面的第 16 行,我们将 5 英尺转换为了厘米度量,并将其安全的转换了回去,得到了原来的值。另外
这个例子还说明我们可以将一个函数的返回值作为另一个函数的参数传入。我们先在第 16 行这里停一下,
考虑一下具体的执行情况。我们传入了{inch,5}的函数返回的结果成功的匹配了模块中的
convert_length({centimeter,X}),原因在于前一个函数的返回是{centimeter,X}形式的。如果还不够清楚,那
么你可以分别执行这两个函数,仔细看看他们的返回情况。
我们看了有两个部分的元组,但是元组可以有更多的部分组成,我们可以包含任何合法的 Erlang 内容。例
如,为了表示城市的温度,我们写下如下代码:
{moscow, {c, -10}}
{cape_town, {f, 70}}
{paris, {f, 28}}
元组有固定的内部的组成数量。我们称在元组中的东西为元素。所以元组{moscow,{c,-10}},元素 1 为
moscow,元素 2 为{c,-10}。这里的 c 表示摄氏度,f 为华氏度。
1.5 列表
元组将各种元素组合在一起,我们同样也希望能够表示一串某些东西。
列表在 Erlang 中就是被“[”和“]”包围起
来的部分。下面就是一个关于城市和对应气温的列表的例子:
[{moscow, {c, -10}}, {cape_town, {f, 70}}, {stockholm, {c, -4}},
{paris, {f, 28}}, {london, {f, 36}}]
注意到这个列表很长,以致于一行不能显示完,这没有关系,Erlang 允许在非敏感的地方换行,但是在一
个常量中或者整数中换行就是不允许的。
一个非常有用的寻找列表中一部分的方法就是使用“|”。在下面的 shell 中的例子可以很好的进行说明:
18> [First |TheRest] = [1,2,3,4,5].
[1,2,3,4,5]
19> First.
1
20> TheRest.
[2,3,4,5]
我们使用“|”来分开第一个列表元素与后面剩下的元素。(First 可以用来获取到值“1”,而 TheRest 可以获取
到剩下的部分[2,3,4,5])。
另外一个例子:
21> [E1, E2 | R] = [1,2,3,4,5,6,7].
[1,2,3,4,5,6,7]
22> E1.
1
23> E2.
2
24> R.
[3,4,5,6,7]
从上面我们可以看到使用“|”可以从列表中分离最开始的两个元素。
当然,如果我们尝试获取比列表中包含的
元素数量更多的元素,我们只会得到一个错误提示。
但是有一个特殊情况就是获得一个没有包含任何元素的
空列表[]。
25> [A, B | C] = [1, 2].
[1,2]
26> A.
1
27> B.
2
28> C.
[]
从上面所有的例子中,我们使用了新的变量名,而没有使用已经用过的那些:First、
TheRest、 E2、 A、
E1、 R、
B、 原因在于在上下文相关的地方(同一个作用域),我们的变量只能赋值一次。
C。
我们稍后会回到这里,
它并不想听上去那么奇怪。
下面的例子展示了我们如何得到一个列表的长度:
-module(tut4).
-export([list_length/1]).
list_length([]) ->
0;
list_length([First | Rest]) ->
1 + list_length(Rest).
编译文件“tut4.erl”并且运行一下:
29> c(tut4).
{ok,tut4}
30> 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 部分的长度的和。
(对于高级一点的读者:这里并非只能使用递归的方式,也有一些更好的方式来实现哈。)
一般情况下我们可以在我们在其他语言中使用“记录 records”或者“结构 structs”的地方说我们使用了列表,
特别是我们试图表现一些可变大小的数据时。(就如同我们在其他语言中使用链表一样)
Erlang 没有一个字符串数据类型,取而代之的是使用 ASCII 表示的列表。所以列表[97,98,99]等效为字符串
“abc”。Erlang Shell 是一个聪明的系统,可以猜出我们的列表到底想要表达什么样的数据,并且以合适的方
式进行输出,这在大多数场合都是适用的。例如:
31> [97,98,99].
"abc"
1.6 标准模块和手册页面
Erlang 有很多标准模块帮助我们做一些常见的事情。例如,模块 io 包含了很多函数帮助我们格式化输入和
输出。搜寻这些标准模块的信息,我们可以使用命令 erl-man(可以是操作系统的提示符或者 Erlang 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 发行版本中的文档吧,或者是从
www.erlang.se 网站上下载文档(html 或者 pdf 格式的)或者是 www.erlang.org 上面也有。下面是 R98 的
文档地址:
http://www.erlang.org/doc/r9b/doc/index.html
1.7 输出到终端
在下面的例子中我们可以很好的将格式化的结果输出到终端,我们将从中学习如何使用 io:format 函数。当
然,和其他很多函数一样,我们可以在 shell 中测试这些函数的实际效果:
32> io:format("hello world~n", []).
hello world
ok
33> io:format("this outputs one Erlang term: ~w~n", [hello]).
this outputs one Erlang term: hello
ok
34> io:format("this outputs two Erlang terms: ~w~w~n", [hello, world]).
this outputs two Erlang terms: helloworld
ok
35> 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:fomrat/2 函数如果运行一切正常的话,自己返回一个常量
ok。
如同其他 Erlang 中的函数一样,如果发生什么错误将会直接提示出错信息。
这并不是 Erlang 的错误或者
缺陷,只是一个经过深思熟虑的策略。
Erlang 有一个经过长期检验的实现机制来捕获错误,我们稍后会深入
的讨论相关的内容。作为一个联系,我们尝试让 io:format 挂掉,这应该不难,在这个过程中 Erlnag 本身是
不会挂掉的。
1.8 一个大一些的例子
现在有一个大一些的例子帮助我们更加深入的学习。这里我们准备了一个从一组城市中读取气温的列表。其
中一些是摄氏度,另一些是华氏度的表示,让我们以更加适于阅读的方式输出这些信息:
%% 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]).
36> c(tut5).
{ok,tut5}
37> 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.1111 c
stockholm
-4 c
paris
-2.22222 c
london
2.22222 c
ok
在我们关注程序怎样运行之前,注意我们在代码中添加了一些注释信息。注释都是以%开头的。注意export([format_temps/1]).这行意味着包含一个函数 format_temps/1,其他函数都是本地函数,也就是说在
模块 tut5 之外是看不到这些函数的。
同样注意我们在 shell 中运行这个程序的情况,我们的输入跨越了两行,因为太长了,这也是允许的。
当调用首次 format_temps 的时候,City 获得值{moscow,{c,-10}},并且 Rest 保存着剩余的列表。然后我们
调用函数 print_temp(covert_to_celsius({moscow,{c,-10}}))。
convert_to_celsius({moscow,{c,-10}})是作为 print_temps 的参数。
这个嵌套的函数的执行是从内到外的一个
过程。首先我们执行 convert_to_celsius({moscow,{c,-10}}),因为我们传入的参数已经是使用摄氏度的表达
形式了,所以下面我们马上紧接着执行 print_temp({moscow,{c,-10}})。函数 convert_to_celsius 的工作情况
与前面的例子 convert_length 很类似。
print_temp 函数简单的调用了 io:format 函数。
这里的~-15w 表达的意思是打印传入的值,但是限定长度(或
者说是数据宽度)必须小于 15,数据左对齐。
现在我们调用 format_temps(Rest),将剩余的部分作为参数传入。
这个工作的方式很类似于其他语言中的循
环结构。(这里是一个递归的过程,不用太“害怕”哈)。同样的,format_temps 函数被再次调用,这次 City
得到值{cape_town,{f,70}},我们重复上面说过的过程进行数据的处理。
我们持续的运行该程序,直到列表为
空。当列表为空时,执行第一个子句 format_temps([]),程序简单的返回一个常量 ok,至此,程序顺利结束。
1.9 变量的匹配、限定和作用域
我们可能需要寻找在列表中的最高和最低温度。
在我们动手扩展我们前面的程序之前,让我们来看一个在列
表中寻找最大值的元素的函数:
-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).
39> c(tut6).
{ok,tut6}
40> 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 条件满足的时候使用 Result_so_far 替
代原先的 Head 的值。->前面的 when 是一个关键词,告诉我们如果条件为真则执行函数的这一部分,我们
将这种测试过程称为界定(guard)。如果一个条件界定不为真(也就是说界定失败),我们尝试运行函数
的另一部分。
在这个例子中如果 Head 不是大于 Result_so_far,也就是 Head 等于或者小于 Resutl_so_far,
所以我们就不需要在函数的另一部分再加入一个界定来判断执行条件了。
一些有用的界定符例如“<小于”,“>大于”,“==等于”,“>=不小于”,“<=不大于”,“/=不等于”。
改变上面的程序,使其变为寻找列表中最小值的函数,我们只需要将<替换为>符号就可以了。
(除此之外似
乎还应该将函数名称改为 list_min 更为恰当 :))
记得我曾经在较早前提起过变量只能在作用域内被赋值一次吗?在上面我们看到,Result_so_far 被重复的
赋值了很多次,这是合法的,因为每次我们调用 list_max/2 的时候系统都会新建一个作用域并且每个作用
域内的变量都是完全不一样的。
另一种创建和赋值给变量的方法是使用操作符=。
所以如果我写下 M=5,一个叫做 M 的变量就会被创建然后
被赋值为 5.如果我又在同一个作用域中写 M=6,我们将得到一个错误提示。在 shell 中尝试下面的代码:
41> M = 5.
5
42> M = 6.
** exited: {{badmatch,6},[{erl_eval,expr,3}]} **
43> M = M + 1.
** exited: {{badmatch,6},[{erl_eval,expr,3}]} **
44> N = M + 1.
6
匹配操作符在分离和创建新的变量方面有独特的作用:
45> {X, Y} = {paris, {f, 28}}.
{paris,{f,28}}
46> X.
paris
47> Y.
{f,28}
这里我们的 X 得到了值 pairs,Y 得到了{f,28}。
当然,如果我们尝试对其他的城市重复上面的操作又不改变变量,那么会得到出错信息:
49> {X, Y} = {london, {f, 36}}.
** exited: {{badmatch,{london,{f,36}}},[{erl_eval,expr,3}]} **
变量能够用来提高程序的可阅读性,例如,上面的 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);
这样的写法可能更加清晰一点。
1.10 进一步讨论列表
记得“|”操作符在获取列表头部的作用吗?
50> [M1|T1] = [paris, london, rome].
[paris,london,rome]
51> M1.
paris
52> T1.
[london,rome]
“|”操作符也可以用来为列表增加一个头元素:
53> L1 = [madrid | T1].
[madrid,london,rome]
54> 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.
56> c(tut8).
{ok,tut8}
57> 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 包含了很多对列表进行操作的函数,例如翻转列表,在我们自己动手写一个函数之前,最好还是
首先检查一下有没有在模块中已经为我们准备好的函数。
现在我们会过头来看看城市和温度的问题,但是这次将更加结构化一点。
首先让我们把整个列表转化为摄氏
度表示,并且测试下面的函数:
-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([]) ->
[].
58> c(tut7).
{ok, tut7}.
59> tut7:format_temps([{moscow, {c, -10}}, {ca pe_town, {f, 70}},
{stockholm, {c, -4}}, { paris, {f, 28}}, {london, {f, 36}}]).
[{moscow,{c,-10}},
{cape_town,{c,21.1111}},
{stockholm,{c,-4}},
{paris,{c,-2.22222}},
{london,{c,2.22222}}]
我们一点一点的看:
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.
60> c(tut7).
{ok,tut7}
61> tut7:format_temps([{moscow, {c, -10}}, {ca pe_town, {f, 70}},
{stockholm, {c, -4}}, { paris, {f, 28}}, {london, {f, 36}}]).
moscow
-10 c
cape_town
21.1111 c
stockholm
-4 c
paris
-2.22222 c
london
2.22222 c
ok
我们现在已经添加了一个函数去寻找有最高气温和最低气温的城市了。这个程序并不是最有效率的,我们 4
次遍历整个列表,但是对于清晰程度和正确性来说却没有太大的影响,我们只在确实需要优化性能的时候
进行优化:
-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]).
62> c(tut7).
{ok, tut7}
63> tut7:format_temps([{moscow, {c, -10}}, {ca pe_town, {f, 70}},
{stockholm, {c, -4}}, { paris, {f, 28}}, {london, {f, 36}}]).
moscow
-10 c
cape_town
21.1111 c
stockholm
-4 c
paris
-2.22222 c
london
2.22222 c
Max temperature was 21.1111 c in cape_town
Min temperature was -10 c in moscow
ok
1.11 If 和 Case 语句
函数 find_max_and_min 将为我们找到最高和最低气温。我们在这里引入了一个新的关键字 if,它的工作情
况如下:
if
Condition 1 ->
Action 1;
Condition 2 ->
Action 2;
Condition 3 ->
Action 3;
Condition 4 ->
Action 4
end
注意,在 end 前面的最后一个条件是没有“;”的!这里的判定条件和界定(Guard)是一样的,测试条件的
真或假。
Erlang 从最高处开始执行,直到它找到一个为真的条件,并执行其内部的代码,并且很重要的是它
将忽略其他剩下的条件,不论其他剩下的条件中是否还有为真的情况。
一个条件当是常量的时候意味着永远
为真,true 和常量(atoms)常常用来作为 if 的最后一个条件。作为当其他所有条件都为假时的执行出口。
下面是一个简短的程序,用来表现 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 ->
%i.e. A equals 2 and B equals 3
io:format("A == 2, B == 3~n", []),
a_equals_2_b_equals_3;
A == 1 ; B == 7 ->
%i.e. A equals 1 or B equals 7
io:format("A == 1 ; B == 7~n", []),
a_equals_1_or_b_equals_7
end.
下面是对程序的测试:
64> c(tut9).
{ok,tut9}
65> tut9:test_if(5,33).
A=5
a_equals_5
66> tut9:test_if(33,6).
B=6
b_equals_6
67> tut9:test_if(2, 3).
A == 2, B == 3
a_equals_2_b_equals_3
68> tut9:test_if(1, 33).
A == 1 ; B == 7
a_equals_1_or_b_equals_7
69> tut9:test_if(33, 7).
A == 1 ; B == 7
a_equals_1_or_b_equals_7
70> tut9:test_if(33, 33).
=ERROR REPORT==== 11-Jun-2003::14:03:43 ===
Error in process <0.85.0> with exit value:
{if_clause,[{tut9,test_if,2},{erl_eval,exprs,4},{shell,eval_loop,2}]}
** exited: {if_clause,[{tut9,test_if,2},
{erl_eval,exprs,4},
{shell,eval_loop,2}]} **
注意到执行 tut9:test_if(33,33)时由于没有任何条件可以满足时出现了错误 if_clause。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.
71> c(tut10).
{ok,tut10}
72> tut10:convert_length({inch, 6}).
{centimeter,15.2400}
73> tut10:convert_length({centimeter, 2.5}).
{inch,0.98425}
注意 case 和 if 都有返回值,在上面的例子中 case 返回{inch,X/2.54}或者{centimeter,Y*2.54}。case 的行为
可以被界定(Guard)修改。一个例子可能能够更加清楚的说明这个问题。下面的例子告诉我们给定了年份
时某个月份的天数。我们需要知道年份是理所应当的,毕竟有闰年的情况需要处理嘛:
-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.
74> c(tut11).
{ok,tut11}
75> tut11:month_length(2004, feb).
29
76> tut11:month_length(2003, feb).
28
77> tut11:month_length(1947, aug).
31
1.12 内建函数 BIFs
内建函数 Bifs 是一些处于某些理由构建在 Erlang 虚拟机内部的函数。BIFs 常常实现功能性的操作,而这些
操作可能是很难在 Erlang 中直接实现的,或者说是实现起来没有效率的。一些 BIFs 可以被通过函数名进行
调用,它们这时是默认属于 Erlang 模块的,例如上面看到的 trunc 函数其实是 erlang:trunc。
如你所见,我们首先找出某一年是否是闰年。
如果某一年可以被 400 整除,则是闰年。
为了找到能被 400 整
除的年份,我们使用了内建函数 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
于是,这就的到一个闰年了。
接下来的两个测试是如果可以被 100 或者 4 整除,也是闰年,实现的过程很类
似。
第一个 if 返回 leap 或者 not_leap(当时闰年的时候返回 leap)。
我们使用这个变量来界定二月份的日期
长度情况。
这个例子展示了如何使用 trunc 函数,我们使用另外一个操作符 rem 能够轻松的得到余数,看例子:
2> 2004 rem 400.
4
我们写的是:
trunc(Year / 400) * 400 == Year ->
leap;
改写为:
Year rem 400 == 0 ->
leap;
这里有很多的内建函数 BIFs,但是只有一些 BIFs 可以作为界定来使用,并且你不能使用自定义的函数作
为界定。(对于高级一点的读者:这里需要注意界定是没有副作用的)。让我们看看这些 BIFs 是怎样的:
78> trunc(5.6).
5
79> round(5.6).
6
80> length([a,b,c,d]).
4
81> float(5).
5.00000
82> is_atom(hello).
true
83> is_atom("hello").
false
84> is_tuple({paris, {c, 30}}).
true
85> is_tuple([paris, {c, 30}]).
false
上面的所有 BIFs 都可以作为界定。而下面的这些则不行:
87> atom_to_list(hello).
"hello"
88> list_to_atom("goodbye").
goodbye
89> integer_to_list(22).
"22"
上面的 3 个 BIFs 可以帮助我们完成一些在 Erlang 中很困难甚至是不可能的任务。
1.13 更高端的函数(Funs)
Erlang 和其他的函数式编程语言一样,有一些高端的函数,我们下面就来看看这部分的内容:
90> Xf = fun(X) -> X * 2 end.
#Fun<erl_eval.5.123085357>
91> Xf(5).
10
我们在这里定义了一个函数,其功能是将输入的数值乘以 2.于是我们调用 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:
92> Add_3 = fun(X) -> X + 3 end.
#Fun<erl_eval.5.123085357>
93> lists:map(Add_3, [1,2,3]).
[4,5,6]
现在让我们输出城市的温度列表:
95> Print_City = fun({City, {X, Temp}}) -> io:format(" ~-15w ~w ~w~n",
[City, X, Temp]) end.
#Fun<erl_eval.5.123085357>
96> 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).
98> tut13:convert_list_to_c([{mosco w, {c, -10}}, {cape_to wn, {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)。所以在 map 中我们写 lists:map(fun convert_to_c/1,List)。所以你可以看到
convert_list_to_c 变短了,变得更加容易阅读理解了。
标准模块 lists 同样包含了函数 sort(Fun,List),这里的 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).
99> c(tut13).
{ok,tut13}
100> tut13:convert_list_to_c([{mosco w, {c, -10}}, {cape_to wn, {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,
这里我们引入了一个概念——匿名变量"_"(anonymous variable)。
这是一种当获取一个值时的缩写的形式,
但是我们将忽略这个值。我们可以在任何地方使用这个匿名特性,不仅仅是在 fun 中。Temp1<Temp2 返回
true,如果 Temp1 小于 Temp2 的话。
2 并发编程
2.1 进程
使用 Erlang 而不是其他函数式语言的一个很主要的原因就是 Erlang 具有处理并发和分布式计算的编程能力。
我们这里说的并发是指程序可以在同一个时点处理多个线程的执行。例如,现代操作系统可以允许你使用
Word 的同时使用 Excel,并且还开着一个电子邮件客户端程序,一个打印的任务在后台也在执行着。当然,
对于系统中的每个处理器(CPU)来说同一时刻只能处理一个线程(任务),但是当系统以一定的速率在
不同的线程之间进行切换的时候,给人类的感觉就是这些任务都是在同一时间执行的。在一个 Erlang 程序
中很容易创建一个线程进行并发、
并行的执行操作,线程之间的通讯也是非常容易的。 Erlang 系统中,我
在
们称每一个执行的线程为 Process(注意这里的特殊性,不要与其他系统中的进程相混淆)。
(注意:专有名词“Process”经常用于当执行的线程不与别的线程进行数据共享的情况对这种线程的称呼。
当这些线程之间共享数据时,我们一般把它们看作一个整体,作为一个 Process 进程。在 Erlang 中,我们
往往称呼不共享数据的 Thread 为 Process,并且很多时候是混合着叫的,读者应该自己从上下文中进行分
辨)
Erlang 的内建函数 spawn 被用来创建一个新的进程:spawn(Module,Exported_Function,List of
Arguments)。考虑下面的例子:
-module(tut14).
-export([start/0, say_something/2]).
say_something(What, 0) ->
done;
say_something(What, Times) ->
io:format("~p~n", [What]),
say_something(What, Times - 1).
start() ->
spawn(tut14, say_something, [hello, 3]),
spawn(tut14, say_something, [goodbye, 3]).
5> c(tut14).
{ok,tut14}
6> tut14:say_something(hello, 3).
hello
hello
hello
done
我们可以看到函数 say_something 的第一个参数表示要“说的话”,第二个参数表示说话的次数。现在我们来
看看 start 函数,它首先启动两个 Erlang 进程(注意:这里的进程和操作系统的进程并不是一回事,有很大
的差别,具体的内容我们会在后面的内容中进行介绍),一个负责输出“hello”3 次,另一个负责输出
“goodbye”3 次。
这些进程都是用函数 sya_something。
注意这个被 spawn 函数所使用的函数必须从模块中暴
露出来,也就是说必须在模块中使用了-export 语句暴露的函数。
9> tut14:start().
hello
goodbye
<0.63.0>
hello
goodbye
hello
goodbye
注意这里并不是首先输出“hello”三次,然后再输出“googdbye”三次,而是交替出现的。这里的<0.63.0>(不
同的运行环境和机器都会有不同的具体数值哈)从何而来?这个函数返回值当然是最后一件“事情”的返回值。
在 start 函数中的最后一件事情是:
spawn(tut14, say_something, [goodbye, 3]).
函数 spawn 返回一个进程标识符(也就是耳熟能详的 PID),用来唯一表示一个进程的。所以<0.63.0>是
spawn 函数被调用后返回的 pid。我们将在下面的例子中使用到 pid。
同样注意到我们这里在函数 io:format 中使用了“~p”代替“~w”。“~p”大体上和“~w”输出是一致的,但是会将过
长的可打印的词组切分为多行,并且明显的缩进每行。这也是将可打印字符作为字符串输出的常见方法。
2.2 消息传递
下面的例子中,我们创建了两个进程,其中一个重复向另一个发送消息。
-module(tut15).
-export([start/0, ping/2, pong/0]).
ping(0, Pong_PID) ->
Pong_PID ! finished,
io:format("ping finished~n", []);
ping(N, Pong_PID) ->
Pong_PID ! {ping, self()},
receive
pong ->
io:format("Ping received pong~n", [])
end,
ping(N - 1, Pong_PID).
pong() ->
receive
finished ->
io:format("Pong finished~n", []);
{ping, Ping_PID} ->
io:format("Pong received ping~n", []),
Ping_PID ! pong,
pong()
end.
start() ->
Pong_PID = spawn(tut15, pong, []),
spawn(tut15, ping, [3, Pong_PID]).
1> c(tut15).
{ok,tut15}
2> tut15: start().
<0.36.0>
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
ping finished
Pong finished
函数 start 首先创建了一个进程,我们叫它做“pong”:
Pong_PID = spawn(tut15, pong, [])
这个进程执行 tut15:pong()。Pong_PID 是这个进程“pong”的标识符。函数 start 现在要创建另一个进程“ping”
了。
spawn(tut15, ping, [3, Pong_PID]),
这个进程执行:
tut15:ping(3, Pong_PID)
<0.36.0> 是函数 start 的返回值。
进程“pong”现在做:
receive
finished ->
io:format("Pong finished~n", []);
{ping, Ping_PID} ->
io:format("Pong received ping~n", []),
Ping_PID ! pong,
pong()
end.
receive 关键词被用来让进程等待从其他进程发来的消息,格式如下:
receive
pattern1 ->
actions1;
pattern2 ->
actions2;
....
patternN
actionsN
end.
注意:在 end 之前没有“;”。
在 Erlang 进程之间传递的消息都是简单的合法的 Erlang“短语(Term)”。
可以是列表、
元组、
整数、
常量或者
pid 什么的。
每个进程都有自己的输入消息队列,用以接受消息。新的消息到达该进程时被放在队列的末尾。当一个进程
执行一个 receive,队列中的第一个消息被 receive 中的第一个模式(pattern)匹配测试,如果匹配,该消
息从消息队列中删除,并且执行对应 pattern 下的操作。
如此,如果第一个 pattern 没有被匹配成功,第二个模式就将被测试,如果匹配成功,该消息就会从消息队
列中删除,并且执行对应 pattern 下的操作。
如果第二个 pattern 仍然不能被测试为真,则该过程以此类推,
直到没有 pattern 可供测试为止。
如果没有 pattern 可供测试了,第一个消息将被保存在在消息队列中,并且
开始第二个消息的匹配测试工作,如果这时第二个消息匹配成功了,则删除消息队列中的第二个消息,但
是第一个消息和其他消息的状态并不受到任何影响。
如果第二个消息还是匹配失败,则该过程持续下去,依
次进行第三个、第四个消息的匹配。如果我们到了队列的末尾,该进程被阻塞(停止执行),并且等待新消
息的到来,然后重新开始匹配的过程。
当然,Erlang 的实现是非常“聪明”的,并且能够最小化每个消息被接收方的 receive 测试的次数。
现在回到我们的 ping pong 例子。
"Pong" 等待着消息。
如果常量 finished 被接收到,“pong”将输出“Pong finished”,然后继续“无所事事”。
如果
它接收一个消息是下面的格式:
{ping, Ping_PID}
它输出“Pong received ping”,并且发送常量 pong 到进程“ping”:
Ping_PID ! pong
注意:操作符“!”怎样被用来发送消息的,下面是“!”的语法:
Pid ! Message
消息(可以是任何的 Erlang 的 Term)被用来向 Pid 标识的的进程发送消息。
在 pong 发送消息到进程“ping”后,“pong”再次调用了 pong 函数,这就让它回到了等待接受消息的那种状态,
从而等待其他消息的到来。现在我们来看进程“ping”。回忆它是怎么开始运行的:
tut15:ping(3, Pong_PID)
看看 ping/2,我们看到 ping/2 的第二个子句被执行,并且带有参数 3(不是 0)(第一个子句是
ping(0,Pong_PID),第二个子句是 ping(N,Pong_PID),这里的 N 会被赋值为 3)。
第二个子句发送到消息到“pong”:
Pong_PID ! {ping, self()},
self() 返回当前执行 self()进程的 pid,在这里就是“ping”的 pid。(回忆“pong”的代码,看看里面 Pong_PID
的情况)
"Ping"现在等待着从“pong”传回的信息:
receive
pong ->
io:format("Ping received pong~n", [])
end,
并且当回复的消息到达时会输出“Ping received pong”,之后“ping”会再次调用 ping 函数。
ping(N - 1, Pong_PID)
N-1 使得第一个参数递减,直到减到 0 为止。当减到 0 时,第一个子句 ping/2 会被执行:
ping(0, Pong_PID) ->
Pong_PID ! finished,
io:format("ping finished~n", []);
常量 finished 被发送给“pong”(这将导致接受方终止)并且输出“ping finished”。
“ping”然后自己结束掉自己。
2.3 注册进程名称
在上面的例子中,我们首先创建了“pong”,然后在启动“ping”之前给“pong”一个唯一标识符。某些时候“ping”
必须直到“pong”的标识符才能够发送消息给它。某些时候进程需要知道对方的情况,但是进程之间又是完全
独立的。所以为了解决分配 pid 和互相识别的困难,Erlang 提供了一个赋予进程以名字的机制,我们能够通
过使用进程名字来代替 pid 的使用。这必须使用到内建函数 register:
register(some_atom, Pid)
我们现在重写我们的 ping pong 例子,我们将赋予 ping 和 pong 进程以名字:
-module(tut16).
-export([start/0, ping/1, pong/0]).
ping(0) ->
pong ! finished,
io:format("ping finished~n", []);
ping(N) ->
pong ! {ping, self()},
receive
pong ->
io:format("Ping received pong~n", [])
end,
ping(N - 1).
pong() ->
receive
finished ->
io:format("Pong finished~n", []);
{ping, Ping_PID} ->
io:format("Pong received ping~n", []),
Ping_PID ! pong,
pong()
end.
start() ->
register(pong, spawn(tut16, pong, [])),
spawn(tut16, ping, [3]).
2> c(tut16).
{ok, tut16}
3> tut16:start().
<0.38.0>
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
ping finished
Pong finished
在函数 start/0 中,
register(pong, spawn(tut16, pong, [])),
将名称“pong”赋予了进程“pong”。在进程“ping”中我们可以直接使用 pong 来对它发送消息:
pong ! {ping, self()},
所以这里的 ping/2 可以简化为 ping/1,不再使用参数 Pong_PID。
2.4 分布式编程
现在我们来重写 ping pong 例子,其中 ping 和 pong 都在不同的计算机上。在我们做这件事情之前,这里有
一些事情需要首先做到。分布式 Erlang 的实现提供了自有的安全机制来预防未经授权的 Erlang 系统访问 。
Erlang 系统与别的机器进行交互时必须有同样的 magic cookie(魔法甜饼)。
最简单的实现方法就是建立一
个.erlang.cookie 文件在 home 目录,并且复制到其他所有打算运行 Erlang 系统的机器上(如果是用的是
Windows 系统,那么就是环境变量$HOME 所指的目录,可能需要我们手动进行设定),在 UNIX 或者
LINUX 系统下可以安全的忽略上面的东西并且只需要简单的创建一个.erlang.cookie 文件就可以了。该文件
需要包含一行常量,例如在 LINUX 系统上,我们可以这样设定:
$ cd
$ cat > .erlang.cookie
this_is_very_secret
$ chmod 400 .erlang.cookie
这个 chmod 命令让.erlang.cookie 的文件访问许可只能限定于文件的所有者,这是必须的。
当我们启动一个 Erlang 系统并且试图与别的 Erlang 系统进行交互时,我们必须给定名称:
erl -sname my_name
我们稍后将看到更多的细节。如果你打算尝试分布式 Erlang,但是只有一台计算机,你可以同时打开两个
Erlang 环境在同一台计算机上,然后赋予他们不同的名称就可以了(使用上面的操作系统 shell 下的命令)。
每个运行在计算机上的 Erlang 系统都被称为一个节点(Node)。
(注意:erl -sname 命令假定所有节点都在同一个 IP 域内,如果我们需要在不同的域内使用,就用-name
参数代替原有的参数,但是必须给出所有的 IP 地址)
这里是修改后分别运行在不同节点上的 ping pong 例子:
-module(tut17).
-export([start_ping/1, start_pong/0, ping/2, pong/0]).
ping(0, Pong_Node) ->
{pong, Pong_Node} ! finished,
io:format("ping finished~n", []);
ping(N, Pong_Node) ->
{pong, Pong_Node} ! {ping, self()},
receive
pong ->
io:format("Ping received pong~n", [])
end,
ping(N - 1, Pong_Node).
pong() ->
receive
finished ->
io:format("Pong finished~n", []);
{ping, Ping_PID} ->
io:format("Pong received ping~n", []),
Ping_PID ! pong,
pong()
end.
start_pong() ->
register(pong, spawn(tut17, pong, [])).
start_ping(Pong_Node) ->
spawn(tut17, ping, [3, Pong_Node]).
让我们假设我们有两台计算机分别叫做 gollum 和 kosken。我们首先打开在计算机 kosken 上打开一个节点,
称为 ping,然后打开 gollum,叫做 pong。
在 kosken 上(在一台 Linux 系统上):
kosken> erl -sname ping
Erlang (BEAM) emulator version 5.2.3.7 [hipe] [threads:0]
Eshell V5.2.3.7 (abort with ^G)
(ping@kosken)1>
在 gollum 上(在一台 Windows 系统上) :
gollum> erl -sname pong
Erlang (BEAM) emulator version 5.2.3.7 [hipe] [threads:0]
Eshell V5.2.3.7 (abort with ^G)
(pong@gollum)1>
现在我们在 gollum 上开始“pong”进程:
(pong@gollum)1> tut17:start_pong().
true
然后再 kosken 上开始“ping”进程(从上面的代码中我们可以看到 start_ping 函数的一个参数是 pong 所运行
的 Erlang 系统的名称):
(ping@kosken)1> tut17:start_ping(pong @gollum).
<0.37.0>
Ping received pong
Ping received pong
Ping received pong
ping finished
这里我们可以看到 ping pong 例子运行起来了,在“pong”一端,我们可以看到:
(pong@gollum)2>
Pong received ping
Pong received ping
Pong received ping
Pong finished
(pong@gollum)2>
看看 tut17 中的代码,我们研究一下 pong 函数,发现并没有和前面的例子有什么改变:
{ping, Ping_PID} ->
io:format("Pong received ping~n", []),
Ping_PID ! pong,
这里的工作与运行“ping”的节点完全无关。Erlang 的 pid 包含了关于进程在何处执行的信息,所以如果你知
道一个进程的 pid,操作符“!”就可以被用来向这个进程传递消息,而不论该进程是否是在本机还是在远程。
一个不同点在于,如果是在另一个节点上的有名称的进程,我们对其发送消息:
{pong, Pong_Node} ! {ping, self()},
我们使用元组{regiestered_name,node_name}替换只是使用 registered_name。
在之前的例子,我们开始“ping”和“pong”与不同的节点。
spawn 可以被用来启动其他节点上的进程。
下面的例
子还是 ping pong 程序,不过我们这次将“远程”启动进程:
-module(tut18).
-export([start/1, ping/2, pong/0]).
ping(0, Pong_Node) ->
{pong, Pong_Node} ! finished,
io:format("ping finished~n", []);
ping(N, Pong_Node) ->
{pong, Pong_Node} ! {ping, self()},
receive
pong ->
io:format("Ping received pong~n", [])
end,
ping(N - 1, Pong_Node).
pong() ->
receive
finished ->
io:format("Pong finished~n", []);
{ping, Ping_PID} ->
io:format("Pong received ping~n", []),
Ping_PID ! pong,
pong()
end.
start(Ping_Node) ->
register(pong, spawn(tut18, pong, [])),
spawn(Ping_Node, tut18, ping, [3, node()]).
假定一个 Erlang 系统叫做“ping”(不是“ping”进程哈)已经在 kosken 上启动了,那么我们在 gollum 上操作:
(pong@gollum)1> tut18:start(ping@kosken).
<3934.39.0>
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong finished
ping finished
注意这里所有的输出都在 gollum 上。这是因为 io 系统会找寻进程创建的位置,并在该位置进行输出。

More Related Content

What's hot

cppcheck源码分析
cppcheck源码分析cppcheck源码分析
cppcheck源码分析
Wu Liang
 
第9章文件
第9章文件第9章文件
第9章文件
summerfeng
 
Arduino應用系統設計 - Arduino程式快速入門
Arduino應用系統設計 - Arduino程式快速入門Arduino應用系統設計 - Arduino程式快速入門
Arduino應用系統設計 - Arduino程式快速入門
吳錫修 (ShyiShiou Wu)
 
Cppcheck分析
Cppcheck分析Cppcheck分析
Cppcheck分析
Wu Liang
 
Hash map导致cpu100% 的分析
Hash map导致cpu100% 的分析Hash map导致cpu100% 的分析
Hash map导致cpu100% 的分析
wang hongjiang
 
系統程式 -- 第 12 章
系統程式 -- 第 12 章系統程式 -- 第 12 章
系統程式 -- 第 12 章
鍾誠 陳鍾誠
 
Inside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutor
Inside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutorInside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutor
Inside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutorAdy Liu
 
Python 入门
Python 入门Python 入门
Python 入门kuco945
 
Net Parallel Programming .NET平行處理與執行序
Net Parallel Programming .NET平行處理與執行序Net Parallel Programming .NET平行處理與執行序
Net Parallel Programming .NET平行處理與執行序HO-HSUN LIN
 
nodeMCU IOT教學02 - Lua語言
nodeMCU IOT教學02 - Lua語言nodeMCU IOT教學02 - Lua語言
nodeMCU IOT教學02 - Lua語言
吳錫修 (ShyiShiou Wu)
 
Php extension开发
Php extension开发Php extension开发
Php extension开发thinkinlamp
 
系統程式 -- 第 11 章
系統程式 -- 第 11 章系統程式 -- 第 11 章
系統程式 -- 第 11 章
鍾誠 陳鍾誠
 
從技術面簡介線上遊戲外掛
從技術面簡介線上遊戲外掛從技術面簡介線上遊戲外掛
從技術面簡介線上遊戲外掛
John L Chen
 
AOP概念及实践
AOP概念及实践AOP概念及实践
AOP概念及实践
Tony Deng
 
Php Extension--yahoo(Cc0cc)
Php Extension--yahoo(Cc0cc)Php Extension--yahoo(Cc0cc)
Php Extension--yahoo(Cc0cc)
sunlick
 
從 REPL 到 IDE
從 REPL 到 IDE從 REPL 到 IDE
從 REPL 到 IDE
Justin Lin
 
线程与并发
线程与并发线程与并发
线程与并发Tony Deng
 
系統程式 - 附錄
系統程式 - 附錄系統程式 - 附錄
系統程式 - 附錄
鍾誠 陳鍾誠
 
Java华为面试题
Java华为面试题Java华为面试题
Java华为面试题yiditushe
 

What's hot (20)

cppcheck源码分析
cppcheck源码分析cppcheck源码分析
cppcheck源码分析
 
Linux chapt3
Linux chapt3Linux chapt3
Linux chapt3
 
第9章文件
第9章文件第9章文件
第9章文件
 
Arduino應用系統設計 - Arduino程式快速入門
Arduino應用系統設計 - Arduino程式快速入門Arduino應用系統設計 - Arduino程式快速入門
Arduino應用系統設計 - Arduino程式快速入門
 
Cppcheck分析
Cppcheck分析Cppcheck分析
Cppcheck分析
 
Hash map导致cpu100% 的分析
Hash map导致cpu100% 的分析Hash map导致cpu100% 的分析
Hash map导致cpu100% 的分析
 
系統程式 -- 第 12 章
系統程式 -- 第 12 章系統程式 -- 第 12 章
系統程式 -- 第 12 章
 
Inside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutor
Inside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutorInside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutor
Inside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutor
 
Python 入门
Python 入门Python 入门
Python 入门
 
Net Parallel Programming .NET平行處理與執行序
Net Parallel Programming .NET平行處理與執行序Net Parallel Programming .NET平行處理與執行序
Net Parallel Programming .NET平行處理與執行序
 
nodeMCU IOT教學02 - Lua語言
nodeMCU IOT教學02 - Lua語言nodeMCU IOT教學02 - Lua語言
nodeMCU IOT教學02 - Lua語言
 
Php extension开发
Php extension开发Php extension开发
Php extension开发
 
系統程式 -- 第 11 章
系統程式 -- 第 11 章系統程式 -- 第 11 章
系統程式 -- 第 11 章
 
從技術面簡介線上遊戲外掛
從技術面簡介線上遊戲外掛從技術面簡介線上遊戲外掛
從技術面簡介線上遊戲外掛
 
AOP概念及实践
AOP概念及实践AOP概念及实践
AOP概念及实践
 
Php Extension--yahoo(Cc0cc)
Php Extension--yahoo(Cc0cc)Php Extension--yahoo(Cc0cc)
Php Extension--yahoo(Cc0cc)
 
從 REPL 到 IDE
從 REPL 到 IDE從 REPL 到 IDE
從 REPL 到 IDE
 
线程与并发
线程与并发线程与并发
线程与并发
 
系統程式 - 附錄
系統程式 - 附錄系統程式 - 附錄
系統程式 - 附錄
 
Java华为面试题
Java华为面试题Java华为面试题
Java华为面试题
 

Viewers also liked

YInN poster
YInN posterYInN poster
C34- ...sound ... - project
 C34-  ...sound ... - project  C34-  ...sound ... - project
C34- ...sound ... - project
Magikdo Basketmz
 
Puentes Elsa
Puentes ElsaPuentes Elsa
Puentes Elsa
biblioteca18de20
 
grammar test
grammar testgrammar test
grammar test
kerlyllerena
 
How can we improve service quality
How can we improve service qualityHow can we improve service quality
How can we improve service quality
Sameer Mathur
 
Chapter 12
Chapter 12Chapter 12
Chapter 12
Fanny Sanchez
 
Scientific research in psychology
Scientific research in psychologyScientific research in psychology
Scientific research in psychology
Rahul singroha
 
Organigrama, correspondencia 2
Organigrama, correspondencia 2Organigrama, correspondencia 2
Organigrama, correspondencia 2
Frankiss Kiss
 
Quoi de neuf à l'Est - le webdoc?
Quoi de neuf à l'Est - le webdoc?Quoi de neuf à l'Est - le webdoc?
Quoi de neuf à l'Est - le webdoc?Storycode Paris
 
50 Employee Issues You Should Be Documenting
50 Employee Issues You Should Be Documenting50 Employee Issues You Should Be Documenting
50 Employee Issues You Should Be Documenting
HR ACUITY LLC
 
Question 2 What are the keys to effective internal marketing? (Chapter 21) ...
Question 2   What are the keys to effective internal marketing? (Chapter 21) ...Question 2   What are the keys to effective internal marketing? (Chapter 21) ...
Question 2 What are the keys to effective internal marketing? (Chapter 21) ...
Sameer mathur
 
Derechos Humanos DIH Responsabilidad De Proteger
Derechos Humanos DIH Responsabilidad De ProtegerDerechos Humanos DIH Responsabilidad De Proteger
Derechos Humanos DIH Responsabilidad De Proteger
dipucvseccions
 
33ºdomingo tc14a
33ºdomingo tc14a33ºdomingo tc14a
33ºdomingo tc14a
CHUC
 
Fundamentacion curriular
Fundamentacion curriularFundamentacion curriular
Fundamentacion curriular
irenelicia
 
Carta CBAN 02
Carta CBAN 02Carta CBAN 02
Carta CBAN 02
Giana Araujo
 
Tecnicas de evaluacion de desempe+æo
Tecnicas de evaluacion de desempe+æoTecnicas de evaluacion de desempe+æo
Tecnicas de evaluacion de desempe+æo
cefic
 
Plano de aula_avestruz
Plano de aula_avestruzPlano de aula_avestruz
Plano de aula_avestruz
Eliane Quintão
 
Sesion2
Sesion2Sesion2
Sesion2
cefic
 
Sandro nuñez
Sandro nuñezSandro nuñez
Sandro nuñez
cefic
 
44646 guia de atividades
44646 guia de atividades44646 guia de atividades
44646 guia de atividades
VanlisaPinheiro
 

Viewers also liked (20)

YInN poster
YInN posterYInN poster
YInN poster
 
C34- ...sound ... - project
 C34-  ...sound ... - project  C34-  ...sound ... - project
C34- ...sound ... - project
 
Puentes Elsa
Puentes ElsaPuentes Elsa
Puentes Elsa
 
grammar test
grammar testgrammar test
grammar test
 
How can we improve service quality
How can we improve service qualityHow can we improve service quality
How can we improve service quality
 
Chapter 12
Chapter 12Chapter 12
Chapter 12
 
Scientific research in psychology
Scientific research in psychologyScientific research in psychology
Scientific research in psychology
 
Organigrama, correspondencia 2
Organigrama, correspondencia 2Organigrama, correspondencia 2
Organigrama, correspondencia 2
 
Quoi de neuf à l'Est - le webdoc?
Quoi de neuf à l'Est - le webdoc?Quoi de neuf à l'Est - le webdoc?
Quoi de neuf à l'Est - le webdoc?
 
50 Employee Issues You Should Be Documenting
50 Employee Issues You Should Be Documenting50 Employee Issues You Should Be Documenting
50 Employee Issues You Should Be Documenting
 
Question 2 What are the keys to effective internal marketing? (Chapter 21) ...
Question 2   What are the keys to effective internal marketing? (Chapter 21) ...Question 2   What are the keys to effective internal marketing? (Chapter 21) ...
Question 2 What are the keys to effective internal marketing? (Chapter 21) ...
 
Derechos Humanos DIH Responsabilidad De Proteger
Derechos Humanos DIH Responsabilidad De ProtegerDerechos Humanos DIH Responsabilidad De Proteger
Derechos Humanos DIH Responsabilidad De Proteger
 
33ºdomingo tc14a
33ºdomingo tc14a33ºdomingo tc14a
33ºdomingo tc14a
 
Fundamentacion curriular
Fundamentacion curriularFundamentacion curriular
Fundamentacion curriular
 
Carta CBAN 02
Carta CBAN 02Carta CBAN 02
Carta CBAN 02
 
Tecnicas de evaluacion de desempe+æo
Tecnicas de evaluacion de desempe+æoTecnicas de evaluacion de desempe+æo
Tecnicas de evaluacion de desempe+æo
 
Plano de aula_avestruz
Plano de aula_avestruzPlano de aula_avestruz
Plano de aula_avestruz
 
Sesion2
Sesion2Sesion2
Sesion2
 
Sandro nuñez
Sandro nuñezSandro nuñez
Sandro nuñez
 
44646 guia de atividades
44646 guia de atividades44646 guia de atividades
44646 guia de atividades
 

Similar to Erlang jiacheng

Erlang开发及应用
Erlang开发及应用Erlang开发及应用
Erlang开发及应用litaocheng
 
Introduce to Linux command line
Introduce to Linux command lineIntroduce to Linux command line
Introduce to Linux command line
Wen Liao
 
用Erlang构建容错系统
用Erlang构建容错系统用Erlang构建容错系统
用Erlang构建容错系统
Cheng Lian
 
Erlang Practice
Erlang PracticeErlang Practice
Erlang Practicelitaocheng
 
Oracle Security 101
Oracle Security 101Oracle Security 101
Oracle Security 101
Dahui Feng
 
Javascript Training
Javascript TrainingJavascript Training
Javascript Training
beijing.josh
 
認識 C++11 新標準及使用 AMP 函式庫作平行運算
認識 C++11 新標準及使用 AMP 函式庫作平行運算認識 C++11 新標準及使用 AMP 函式庫作平行運算
認識 C++11 新標準及使用 AMP 函式庫作平行運算
建興 王
 
Lucene 全文检索实践
Lucene 全文检索实践Lucene 全文检索实践
Lucene 全文检索实践yiditushe
 
Hi Haskell
Hi HaskellHi Haskell
Hi Haskell
Jifeng Deng
 
Clojure简介与应用
Clojure简介与应用Clojure简介与应用
Clojure简介与应用
Robert Hao
 
漫談 Source Control Management
漫談 Source Control Management漫談 Source Control Management
漫談 Source Control Management
Wen-Shih Chao
 
shell script introduction
shell script introductionshell script introduction
shell script introduction
Jie Jin
 
系統程式 -- 第 11 章 嵌入式系統
系統程式 -- 第 11 章 嵌入式系統系統程式 -- 第 11 章 嵌入式系統
系統程式 -- 第 11 章 嵌入式系統
鍾誠 陳鍾誠
 
Learning python in the motion picture industry by will zhou
Learning python in the motion picture industry   by will zhouLearning python in the motion picture industry   by will zhou
Learning python in the motion picture industry by will zhou
Will Zhou
 
Erlang Optimize
Erlang OptimizeErlang Optimize
Erlang Optimize
Feng Yu
 
PHP 也有 Day #35 - 精通 PHP 錯誤處理,讓除錯更自在
PHP 也有 Day #35 - 精通 PHP 錯誤處理,讓除錯更自在PHP 也有 Day #35 - 精通 PHP 錯誤處理,讓除錯更自在
PHP 也有 Day #35 - 精通 PHP 錯誤處理,讓除錯更自在
Asika Simon
 
Linux常用命令与工具简介
Linux常用命令与工具简介Linux常用命令与工具简介
Linux常用命令与工具简介weihe
 

Similar to Erlang jiacheng (20)

Erlang开发及应用
Erlang开发及应用Erlang开发及应用
Erlang开发及应用
 
Introduce to Linux command line
Introduce to Linux command lineIntroduce to Linux command line
Introduce to Linux command line
 
用Erlang构建容错系统
用Erlang构建容错系统用Erlang构建容错系统
用Erlang构建容错系统
 
Erlang Practice
Erlang PracticeErlang Practice
Erlang Practice
 
Golang
GolangGolang
Golang
 
Oracle Security 101
Oracle Security 101Oracle Security 101
Oracle Security 101
 
Javascript Training
Javascript TrainingJavascript Training
Javascript Training
 
C+
C+C+
C+
 
C#
C#C#
C#
 
認識 C++11 新標準及使用 AMP 函式庫作平行運算
認識 C++11 新標準及使用 AMP 函式庫作平行運算認識 C++11 新標準及使用 AMP 函式庫作平行運算
認識 C++11 新標準及使用 AMP 函式庫作平行運算
 
Lucene 全文检索实践
Lucene 全文检索实践Lucene 全文检索实践
Lucene 全文检索实践
 
Hi Haskell
Hi HaskellHi Haskell
Hi Haskell
 
Clojure简介与应用
Clojure简介与应用Clojure简介与应用
Clojure简介与应用
 
漫談 Source Control Management
漫談 Source Control Management漫談 Source Control Management
漫談 Source Control Management
 
shell script introduction
shell script introductionshell script introduction
shell script introduction
 
系統程式 -- 第 11 章 嵌入式系統
系統程式 -- 第 11 章 嵌入式系統系統程式 -- 第 11 章 嵌入式系統
系統程式 -- 第 11 章 嵌入式系統
 
Learning python in the motion picture industry by will zhou
Learning python in the motion picture industry   by will zhouLearning python in the motion picture industry   by will zhou
Learning python in the motion picture industry by will zhou
 
Erlang Optimize
Erlang OptimizeErlang Optimize
Erlang Optimize
 
PHP 也有 Day #35 - 精通 PHP 錯誤處理,讓除錯更自在
PHP 也有 Day #35 - 精通 PHP 錯誤處理,讓除錯更自在PHP 也有 Day #35 - 精通 PHP 錯誤處理,讓除錯更自在
PHP 也有 Day #35 - 精通 PHP 錯誤處理,讓除錯更自在
 
Linux常用命令与工具简介
Linux常用命令与工具简介Linux常用命令与工具简介
Linux常用命令与工具简介
 

Erlang jiacheng

  • 1. Erlang 编程 (第一部分) 1 顺序编程 1.1 The Erlang Shell 大多数操作系统都有一个命令行交互环境或者一个 shell,对于 UNIX 和 LINUX 尤其是这样。Windows 也有 自己的命令行模式。Erlang 也同样有自己的 shell,我们可以直接在里面编写代码和运行我们编写的程序, 并在其中看到我们的输出情况。我们能够在多种操作系统上运行 Erlang 的 shell,一般来说在我们所使用的 操作系统的命令行中输入 erl 就可以了,我们将可能看到下面的提示: % erl Erlang (BEAM) emulator version 5.2 [source] [hipe] Eshell V5.2 (abort with ^G) 1> 现在我们输入“2 + 5.”,(注意最后有一个英文句号,并不包括引号哈) 1> 2 + 5. 7 2> 在 Windows 中,我们还可以通过双击 Erlang shell 的图标来启动 Erlang shell。 你现在注意到了 Erlang shell 的命令行前面都有一个标号(例如 1> 2>),并且下面正确的输出了我们的答 案“7”!同样我们注意到我们的输入的最后有一个英文的句号,这是在我们最终按下回车之前的最后一个字 符。如果我们写在 shell 中的东西有什么错误,我们可以使用退格键进行删除,于是,我们推想我们可以使 用其他 shell 下的一些常用的编辑命令,而事实上确实是这样的,以后我们使用到的时候再介绍。 现在我们尝试一下稍微复杂一点的运算: 2> (42 + 77) * 66 / 3. 2618.00 我们现在使用了一些“复杂一点”的运算符号“*”和“/”,分别表示乘法运算和除法运算。除此之外还支持其他一 些运算符号,我们在以后使用到的时候再介绍。 我们打算关闭 Elrang 系统,则需要在 Erlang shell 中键入 Ctrl+C,我们将看到下面的输出: BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded (v)ersion (k)ill (D)b-tables (d)istribution a % 我们这时候键入“a”就会退出 Erlang 系统。 另一个方法就是键入 halt(),也可以实现退出 Erlang 系统的目的: 3> halt(). % 1.2 Modules 和 Functions (模块和函数) 一个编程语言如果只能让我们从 shell 中运行代码,那么可以说这个语言的用处受到了很大的限制,至少我 会感觉到不爽。 这里有一个小的 Erlang 程序。 我们将下面的内容键入一个叫做 tut.erl 的文件(这里需要注意 到的是我们的 tut.erl 文件应该放在 erl 程序的同一个目录下,文件名应该和模块名相同,这样 Erlang 才能 很好的找到我们的模块,至于编辑器随便一个支持纯文本的就可以哈)。如果我们的编辑器有一个 Erlang 模式就可能编写起来更加方便,并且代码的格式也会变得更加规范,但是我们也许也会产生对这些编辑器 或者 IDE 的强烈依赖性。下面就是我们要键入的代码: -module(tut). -export([double/1]). double(X) -> 2 * X. 不难看出,我们的程序的任务就是将输入的数字进行“乘以 2”的操作。 我们后面会解释前两行的含义。 我们现 在来编译一下这个程序,我们在 Erlang shell 中键入下面的内容:
  • 2. 3> c(tut). {ok,tut} {ok,tut} 告诉我们编译成功的完成了。 如果显示的是“error”,那么可能我们的文件位置或者键入的代码文本存 在某些问题,出错信息会给我们一些提示,我们按照提示做就好了,仔细的检查代码在任何时候都是需要 的:) 现在我们来运行以下这个程序: 4> tut:double(10). 20 和预计的一样,10 的“乘以 2”之后是 20(貌似是废话)。 现在让我们回到最开始的两行代码。Erlang 程序一般是保存在文件中的。每个文件我们在 Erlang 中称为 module(模块)。第一行就是告诉我们这个模块的名字。 -module(tut). 这段代码告诉我们该模块的名称为“tut”。注意这行最后的“.”符号是必不可少的。这个模块名必须和保存这段 代码的文件(后缀为“erl”的文件)有相同的名称。所以这里我们要求了文件名必须是“tut.erl”的原因。当我们 在使用另一个模块中的函数时,我们使用下面的语法 module_name:function_name(arguments).所以: 4> tut:double(10). 意味着我们从 tut 模块中调用一个叫做 double 的函数,并且带有参数“10”. 第二行: -export([double/1]). 含一位模块 tut 中包含一个名叫 double 的函数,并且带有一个参数(就是例子中的“10”),并且这个函数可 以被从模块 tut 之外调用到。更深入的内容我们后面会介绍。现在回头看看上面行尾的句号“.”。 现在有一个稍微复杂一点的例子,是用来进行阶乘(比如 4 的阶乘就是 1*2*3*4 的结果)操作的。键入下面 的代码到一个新建的文本文件,并命名为 tut1.erl: -module(tut1). -export([fac/1]). fac(1) -> 1; fac(N) -> N * fac(N - 1). 编译这个文件: 5> c(tut1). {ok,tut1} 现在我们计算 4 的阶乘: 6> tut1:fac(4). 24 第一部分: fac(1) -> 1; 含义为 1 的阶乘是 1.注意到这部分最后的“;”符号,它预示着下面还有这个函数的其他部分。第二部分: fac(N) -> N * fac(N - 1). 含义是 N 的阶乘等于 N 乘以 N-1 的阶乘。 注意这部分最后的符号“.”,含义为对于该函数已经没有更多的内容 了,函数就此结束。 一个函数可以有很多的参数。让我们扩展这个 tut1 模块,现在多包含一个“白痴函数”:两个数的相乘: -module(tut1). -export([fac/1, mult/2]).
  • 3. fac(1) -> 1; fac(N) -> N * fac(N - 1). mult(X, Y) -> X * Y. 注意到我们也扩展了-export 这行,加入了一个包含两个参数的函数 mult。 编译一下: 7> c(tut1). {ok,tut1} 试一下我们的代码是否正常工作了: 8> tut1:mult(3,4). 12 上面的例子使用了整数进行乘法运算,其中 N/X/Y 成为变量。 注意:变量必须是以大写字母开头,否则编译 会提示出错。下面的变量名就是合法的:Number,ShoeSize,Age 等。 1.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.18110 11> tut2:convert(7, centimeter). 17.7800 注意我们这里使用了十进制的数值(浮点类型),而并没有任何显式的的声明,但是我猜想你们是可以应 付这种情况的。 看一下我们键入其他东西会发生什么情况(除了 inch 和 centimeter 之外的): 13> tut2:convert(3, miles). =ERROR REPORT==== 8-Oct-2006::22:52:46 === Error in process <0.25.0> with exit value: {function_clause,[{tut2,convert, [3,miles]},{erl_eval,expr,3},{erl_eval,exprs,4},{shell,eval_loop,2}]} ** exited: {function_clause,[{tut2,convert,[3,miles]}, {erl_eval,expr,3}, {erl_eval,exprs,4}, {shell,eval_loop,2}]} ** 这里有两部分被称为子句的内容存在于 convert 函数中,但是 miles 并不是这两部分中的其中一部分。于是 Erlang 系统不能成功的匹配函数中的子句调用,于是我们就看到了上面的出错提示 function_clause 消息。 上面的输出看上去是“典型的一团糟”,但是经过我们认真的观察,我们可以清楚的直到代码中到底是发生了 什么。
  • 4. 1.4 元组 现在的 tut2 程序并不具备一个很好的编程代码的风格。考虑下面的代码: tut2:convert(3, inch). 意味着 3 的单位是 inches 英尺?还是 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}. Compile and test: 14> c(tut3). {ok,tut3} 15> tut3:convert_length({inch, 5}). {centimeter,12.7000} 16> tut3:convert_length(tut3:convert_length({inch, 5})). {inch,5.00000} 注意上面的第 16 行,我们将 5 英尺转换为了厘米度量,并将其安全的转换了回去,得到了原来的值。另外 这个例子还说明我们可以将一个函数的返回值作为另一个函数的参数传入。我们先在第 16 行这里停一下, 考虑一下具体的执行情况。我们传入了{inch,5}的函数返回的结果成功的匹配了模块中的 convert_length({centimeter,X}),原因在于前一个函数的返回是{centimeter,X}形式的。如果还不够清楚,那 么你可以分别执行这两个函数,仔细看看他们的返回情况。 我们看了有两个部分的元组,但是元组可以有更多的部分组成,我们可以包含任何合法的 Erlang 内容。例 如,为了表示城市的温度,我们写下如下代码: {moscow, {c, -10}} {cape_town, {f, 70}} {paris, {f, 28}} 元组有固定的内部的组成数量。我们称在元组中的东西为元素。所以元组{moscow,{c,-10}},元素 1 为 moscow,元素 2 为{c,-10}。这里的 c 表示摄氏度,f 为华氏度。 1.5 列表 元组将各种元素组合在一起,我们同样也希望能够表示一串某些东西。 列表在 Erlang 中就是被“[”和“]”包围起 来的部分。下面就是一个关于城市和对应气温的列表的例子: [{moscow, {c, -10}}, {cape_town, {f, 70}}, {stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}] 注意到这个列表很长,以致于一行不能显示完,这没有关系,Erlang 允许在非敏感的地方换行,但是在一 个常量中或者整数中换行就是不允许的。 一个非常有用的寻找列表中一部分的方法就是使用“|”。在下面的 shell 中的例子可以很好的进行说明: 18> [First |TheRest] = [1,2,3,4,5]. [1,2,3,4,5] 19> First. 1 20> TheRest. [2,3,4,5]
  • 5. 我们使用“|”来分开第一个列表元素与后面剩下的元素。(First 可以用来获取到值“1”,而 TheRest 可以获取 到剩下的部分[2,3,4,5])。 另外一个例子: 21> [E1, E2 | R] = [1,2,3,4,5,6,7]. [1,2,3,4,5,6,7] 22> E1. 1 23> E2. 2 24> R. [3,4,5,6,7] 从上面我们可以看到使用“|”可以从列表中分离最开始的两个元素。 当然,如果我们尝试获取比列表中包含的 元素数量更多的元素,我们只会得到一个错误提示。 但是有一个特殊情况就是获得一个没有包含任何元素的 空列表[]。 25> [A, B | C] = [1, 2]. [1,2] 26> A. 1 27> B. 2 28> C. [] 从上面所有的例子中,我们使用了新的变量名,而没有使用已经用过的那些:First、 TheRest、 E2、 A、 E1、 R、 B、 原因在于在上下文相关的地方(同一个作用域),我们的变量只能赋值一次。 C。 我们稍后会回到这里, 它并不想听上去那么奇怪。 下面的例子展示了我们如何得到一个列表的长度: -module(tut4). -export([list_length/1]). list_length([]) -> 0; list_length([First | Rest]) -> 1 + list_length(Rest). 编译文件“tut4.erl”并且运行一下: 29> c(tut4). {ok,tut4} 30> 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 部分的长度的和。 (对于高级一点的读者:这里并非只能使用递归的方式,也有一些更好的方式来实现哈。) 一般情况下我们可以在我们在其他语言中使用“记录 records”或者“结构 structs”的地方说我们使用了列表, 特别是我们试图表现一些可变大小的数据时。(就如同我们在其他语言中使用链表一样)
  • 6. Erlang 没有一个字符串数据类型,取而代之的是使用 ASCII 表示的列表。所以列表[97,98,99]等效为字符串 “abc”。Erlang Shell 是一个聪明的系统,可以猜出我们的列表到底想要表达什么样的数据,并且以合适的方 式进行输出,这在大多数场合都是适用的。例如: 31> [97,98,99]. "abc" 1.6 标准模块和手册页面 Erlang 有很多标准模块帮助我们做一些常见的事情。例如,模块 io 包含了很多函数帮助我们格式化输入和 输出。搜寻这些标准模块的信息,我们可以使用命令 erl-man(可以是操作系统的提示符或者 Erlang 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 发行版本中的文档吧,或者是从 www.erlang.se 网站上下载文档(html 或者 pdf 格式的)或者是 www.erlang.org 上面也有。下面是 R98 的 文档地址: http://www.erlang.org/doc/r9b/doc/index.html 1.7 输出到终端 在下面的例子中我们可以很好的将格式化的结果输出到终端,我们将从中学习如何使用 io:format 函数。当 然,和其他很多函数一样,我们可以在 shell 中测试这些函数的实际效果: 32> io:format("hello world~n", []). hello world ok 33> io:format("this outputs one Erlang term: ~w~n", [hello]). this outputs one Erlang term: hello ok 34> io:format("this outputs two Erlang terms: ~w~w~n", [hello, world]). this outputs two Erlang terms: helloworld ok 35> 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:fomrat/2 函数如果运行一切正常的话,自己返回一个常量 ok。 如同其他 Erlang 中的函数一样,如果发生什么错误将会直接提示出错信息。 这并不是 Erlang 的错误或者 缺陷,只是一个经过深思熟虑的策略。 Erlang 有一个经过长期检验的实现机制来捕获错误,我们稍后会深入 的讨论相关的内容。作为一个联系,我们尝试让 io:format 挂掉,这应该不难,在这个过程中 Erlnag 本身是 不会挂掉的。 1.8 一个大一些的例子 现在有一个大一些的例子帮助我们更加深入的学习。这里我们准备了一个从一组城市中读取气温的列表。其 中一些是摄氏度,另一些是华氏度的表示,让我们以更加适于阅读的方式输出这些信息:
  • 7. %% 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]). 36> c(tut5). {ok,tut5} 37> 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.1111 c stockholm -4 c paris -2.22222 c london 2.22222 c ok 在我们关注程序怎样运行之前,注意我们在代码中添加了一些注释信息。注释都是以%开头的。注意export([format_temps/1]).这行意味着包含一个函数 format_temps/1,其他函数都是本地函数,也就是说在 模块 tut5 之外是看不到这些函数的。 同样注意我们在 shell 中运行这个程序的情况,我们的输入跨越了两行,因为太长了,这也是允许的。 当调用首次 format_temps 的时候,City 获得值{moscow,{c,-10}},并且 Rest 保存着剩余的列表。然后我们 调用函数 print_temp(covert_to_celsius({moscow,{c,-10}}))。 convert_to_celsius({moscow,{c,-10}})是作为 print_temps 的参数。 这个嵌套的函数的执行是从内到外的一个 过程。首先我们执行 convert_to_celsius({moscow,{c,-10}}),因为我们传入的参数已经是使用摄氏度的表达 形式了,所以下面我们马上紧接着执行 print_temp({moscow,{c,-10}})。函数 convert_to_celsius 的工作情况 与前面的例子 convert_length 很类似。 print_temp 函数简单的调用了 io:format 函数。 这里的~-15w 表达的意思是打印传入的值,但是限定长度(或 者说是数据宽度)必须小于 15,数据左对齐。 现在我们调用 format_temps(Rest),将剩余的部分作为参数传入。 这个工作的方式很类似于其他语言中的循 环结构。(这里是一个递归的过程,不用太“害怕”哈)。同样的,format_temps 函数被再次调用,这次 City 得到值{cape_town,{f,70}},我们重复上面说过的过程进行数据的处理。 我们持续的运行该程序,直到列表为 空。当列表为空时,执行第一个子句 format_temps([]),程序简单的返回一个常量 ok,至此,程序顺利结束。 1.9 变量的匹配、限定和作用域 我们可能需要寻找在列表中的最高和最低温度。 在我们动手扩展我们前面的程序之前,让我们来看一个在列 表中寻找最大值的元素的函数: -module(tut6). -export([list_max/1]). list_max([Head|Rest]) ->
  • 8. 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). 39> c(tut6). {ok,tut6} 40> 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 条件满足的时候使用 Result_so_far 替 代原先的 Head 的值。->前面的 when 是一个关键词,告诉我们如果条件为真则执行函数的这一部分,我们 将这种测试过程称为界定(guard)。如果一个条件界定不为真(也就是说界定失败),我们尝试运行函数 的另一部分。 在这个例子中如果 Head 不是大于 Result_so_far,也就是 Head 等于或者小于 Resutl_so_far, 所以我们就不需要在函数的另一部分再加入一个界定来判断执行条件了。 一些有用的界定符例如“<小于”,“>大于”,“==等于”,“>=不小于”,“<=不大于”,“/=不等于”。 改变上面的程序,使其变为寻找列表中最小值的函数,我们只需要将<替换为>符号就可以了。 (除此之外似 乎还应该将函数名称改为 list_min 更为恰当 :)) 记得我曾经在较早前提起过变量只能在作用域内被赋值一次吗?在上面我们看到,Result_so_far 被重复的 赋值了很多次,这是合法的,因为每次我们调用 list_max/2 的时候系统都会新建一个作用域并且每个作用 域内的变量都是完全不一样的。 另一种创建和赋值给变量的方法是使用操作符=。 所以如果我写下 M=5,一个叫做 M 的变量就会被创建然后 被赋值为 5.如果我又在同一个作用域中写 M=6,我们将得到一个错误提示。在 shell 中尝试下面的代码: 41> M = 5. 5 42> M = 6. ** exited: {{badmatch,6},[{erl_eval,expr,3}]} ** 43> M = M + 1. ** exited: {{badmatch,6},[{erl_eval,expr,3}]} ** 44> N = M + 1. 6 匹配操作符在分离和创建新的变量方面有独特的作用: 45> {X, Y} = {paris, {f, 28}}. {paris,{f,28}} 46> X. paris 47> Y.
  • 9. {f,28} 这里我们的 X 得到了值 pairs,Y 得到了{f,28}。 当然,如果我们尝试对其他的城市重复上面的操作又不改变变量,那么会得到出错信息: 49> {X, Y} = {london, {f, 36}}. ** exited: {{badmatch,{london,{f,36}}},[{erl_eval,expr,3}]} ** 变量能够用来提高程序的可阅读性,例如,上面的 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); 这样的写法可能更加清晰一点。 1.10 进一步讨论列表 记得“|”操作符在获取列表头部的作用吗? 50> [M1|T1] = [paris, london, rome]. [paris,london,rome] 51> M1. paris 52> T1. [london,rome] “|”操作符也可以用来为列表增加一个头元素: 53> L1 = [madrid | T1]. [madrid,london,rome] 54> 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. 56> c(tut8). {ok,tut8} 57> 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]
  • 10. 模块 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([]) -> []. 58> c(tut7). {ok, tut7}. 59> tut7:format_temps([{moscow, {c, -10}}, {ca pe_town, {f, 70}}, {stockholm, {c, -4}}, { paris, {f, 28}}, {london, {f, 36}}]). [{moscow,{c,-10}}, {cape_town,{c,21.1111}}, {stockholm,{c,-4}}, {paris,{c,-2.22222}}, {london,{c,2.22222}}] 我们一点一点的看: 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([]) ->
  • 11. []. print_temp([{Name, {c, Temp}} | Rest]) -> io:format("~-15w ~w c~n", [Name, Temp]), print_temp(Rest); print_temp([]) -> ok. 60> c(tut7). {ok,tut7} 61> tut7:format_temps([{moscow, {c, -10}}, {ca pe_town, {f, 70}}, {stockholm, {c, -4}}, { paris, {f, 28}}, {london, {f, 36}}]). moscow -10 c cape_town 21.1111 c stockholm -4 c paris -2.22222 c london 2.22222 c ok 我们现在已经添加了一个函数去寻找有最高气温和最低气温的城市了。这个程序并不是最有效率的,我们 4 次遍历整个列表,但是对于清晰程度和正确性来说却没有太大的影响,我们只在确实需要优化性能的时候 进行优化: -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 ->
  • 12. 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]). 62> c(tut7). {ok, tut7} 63> tut7:format_temps([{moscow, {c, -10}}, {ca pe_town, {f, 70}}, {stockholm, {c, -4}}, { paris, {f, 28}}, {london, {f, 36}}]). moscow -10 c cape_town 21.1111 c stockholm -4 c paris -2.22222 c london 2.22222 c Max temperature was 21.1111 c in cape_town Min temperature was -10 c in moscow ok 1.11 If 和 Case 语句 函数 find_max_and_min 将为我们找到最高和最低气温。我们在这里引入了一个新的关键字 if,它的工作情 况如下: if Condition 1 -> Action 1; Condition 2 -> Action 2; Condition 3 -> Action 3; Condition 4 -> Action 4 end 注意,在 end 前面的最后一个条件是没有“;”的!这里的判定条件和界定(Guard)是一样的,测试条件的 真或假。 Erlang 从最高处开始执行,直到它找到一个为真的条件,并执行其内部的代码,并且很重要的是它 将忽略其他剩下的条件,不论其他剩下的条件中是否还有为真的情况。 一个条件当是常量的时候意味着永远 为真,true 和常量(atoms)常常用来作为 if 的最后一个条件。作为当其他所有条件都为假时的执行出口。 下面是一个简短的程序,用来表现 if 工作的情况: -module(tut9). -export([test_if/2]).
  • 13. 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 -> %i.e. A equals 2 and B equals 3 io:format("A == 2, B == 3~n", []), a_equals_2_b_equals_3; A == 1 ; B == 7 -> %i.e. A equals 1 or B equals 7 io:format("A == 1 ; B == 7~n", []), a_equals_1_or_b_equals_7 end. 下面是对程序的测试: 64> c(tut9). {ok,tut9} 65> tut9:test_if(5,33). A=5 a_equals_5 66> tut9:test_if(33,6). B=6 b_equals_6 67> tut9:test_if(2, 3). A == 2, B == 3 a_equals_2_b_equals_3 68> tut9:test_if(1, 33). A == 1 ; B == 7 a_equals_1_or_b_equals_7 69> tut9:test_if(33, 7). A == 1 ; B == 7 a_equals_1_or_b_equals_7 70> tut9:test_if(33, 33). =ERROR REPORT==== 11-Jun-2003::14:03:43 === Error in process <0.85.0> with exit value: {if_clause,[{tut9,test_if,2},{erl_eval,exprs,4},{shell,eval_loop,2}]} ** exited: {if_clause,[{tut9,test_if,2}, {erl_eval,exprs,4}, {shell,eval_loop,2}]} ** 注意到执行 tut9:test_if(33,33)时由于没有任何条件可以满足时出现了错误 if_clause。case 是另外一种 Erlang 中的判断结构。回忆我们以前写的 convert_length 函数: convert_length({centimeter, X}) -> {inch, X / 2.54}; convert_length({inch, Y}) -> {centimeter, Y * 2.54}.
  • 14. 我们可以改写为: -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. 71> c(tut10). {ok,tut10} 72> tut10:convert_length({inch, 6}). {centimeter,15.2400} 73> tut10:convert_length({centimeter, 2.5}). {inch,0.98425} 注意 case 和 if 都有返回值,在上面的例子中 case 返回{inch,X/2.54}或者{centimeter,Y*2.54}。case 的行为 可以被界定(Guard)修改。一个例子可能能够更加清楚的说明这个问题。下面的例子告诉我们给定了年份 时某个月份的天数。我们需要知道年份是理所应当的,毕竟有闰年的情况需要处理嘛: -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;
  • 15. aug -> 31; oct -> 31; dec -> 31 end. 74> c(tut11). {ok,tut11} 75> tut11:month_length(2004, feb). 29 76> tut11:month_length(2003, feb). 28 77> tut11:month_length(1947, aug). 31 1.12 内建函数 BIFs 内建函数 Bifs 是一些处于某些理由构建在 Erlang 虚拟机内部的函数。BIFs 常常实现功能性的操作,而这些 操作可能是很难在 Erlang 中直接实现的,或者说是实现起来没有效率的。一些 BIFs 可以被通过函数名进行 调用,它们这时是默认属于 Erlang 模块的,例如上面看到的 trunc 函数其实是 erlang:trunc。 如你所见,我们首先找出某一年是否是闰年。 如果某一年可以被 400 整除,则是闰年。 为了找到能被 400 整 除的年份,我们使用了内建函数 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 于是,这就的到一个闰年了。 接下来的两个测试是如果可以被 100 或者 4 整除,也是闰年,实现的过程很类 似。 第一个 if 返回 leap 或者 not_leap(当时闰年的时候返回 leap)。 我们使用这个变量来界定二月份的日期 长度情况。 这个例子展示了如何使用 trunc 函数,我们使用另外一个操作符 rem 能够轻松的得到余数,看例子: 2> 2004 rem 400. 4 我们写的是: trunc(Year / 400) * 400 == Year -> leap; 改写为: Year rem 400 == 0 -> leap; 这里有很多的内建函数 BIFs,但是只有一些 BIFs 可以作为界定来使用,并且你不能使用自定义的函数作 为界定。(对于高级一点的读者:这里需要注意界定是没有副作用的)。让我们看看这些 BIFs 是怎样的: 78> trunc(5.6). 5 79> round(5.6). 6 80> length([a,b,c,d]). 4 81> float(5).
  • 16. 5.00000 82> is_atom(hello). true 83> is_atom("hello"). false 84> is_tuple({paris, {c, 30}}). true 85> is_tuple([paris, {c, 30}]). false 上面的所有 BIFs 都可以作为界定。而下面的这些则不行: 87> atom_to_list(hello). "hello" 88> list_to_atom("goodbye"). goodbye 89> integer_to_list(22). "22" 上面的 3 个 BIFs 可以帮助我们完成一些在 Erlang 中很困难甚至是不可能的任务。 1.13 更高端的函数(Funs) Erlang 和其他的函数式编程语言一样,有一些高端的函数,我们下面就来看看这部分的内容: 90> Xf = fun(X) -> X * 2 end. #Fun<erl_eval.5.123085357> 91> Xf(5). 10 我们在这里定义了一个函数,其功能是将输入的数值乘以 2.于是我们调用 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: 92> Add_3 = fun(X) -> X + 3 end. #Fun<erl_eval.5.123085357> 93> lists:map(Add_3, [1,2,3]). [4,5,6] 现在让我们输出城市的温度列表: 95> Print_City = fun({City, {X, Temp}}) -> io:format(" ~-15w ~w ~w~n", [City, X, Temp]) end. #Fun<erl_eval.5.123085357> 96> lists:foreach(Print_City, [{moscow, {c, -10}}, {cape_town, {f, 70}}, {stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
  • 17. 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). 98> tut13:convert_list_to_c([{mosco w, {c, -10}}, {cape_to wn, {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)。所以在 map 中我们写 lists:map(fun convert_to_c/1,List)。所以你可以看到 convert_list_to_c 变短了,变得更加容易阅读理解了。 标准模块 lists 同样包含了函数 sort(Fun,List),这里的 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). 99> c(tut13). {ok,tut13} 100> tut13:convert_list_to_c([{mosco w, {c, -10}}, {cape_to wn, {f, 70}}, {stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]). [{moscow,{c,-10}}, {stockholm,{c,-4}}, {paris,{c,-2}}, {london,{c,2}},
  • 18. {cape_town,{c,21}}] 在 sort 中我们使用 fun: fun({_, {c, Temp1}}, {_, {c, Temp2}}) -> Temp1 < Temp2 end, 这里我们引入了一个概念——匿名变量"_"(anonymous variable)。 这是一种当获取一个值时的缩写的形式, 但是我们将忽略这个值。我们可以在任何地方使用这个匿名特性,不仅仅是在 fun 中。Temp1<Temp2 返回 true,如果 Temp1 小于 Temp2 的话。 2 并发编程 2.1 进程 使用 Erlang 而不是其他函数式语言的一个很主要的原因就是 Erlang 具有处理并发和分布式计算的编程能力。 我们这里说的并发是指程序可以在同一个时点处理多个线程的执行。例如,现代操作系统可以允许你使用 Word 的同时使用 Excel,并且还开着一个电子邮件客户端程序,一个打印的任务在后台也在执行着。当然, 对于系统中的每个处理器(CPU)来说同一时刻只能处理一个线程(任务),但是当系统以一定的速率在 不同的线程之间进行切换的时候,给人类的感觉就是这些任务都是在同一时间执行的。在一个 Erlang 程序 中很容易创建一个线程进行并发、 并行的执行操作,线程之间的通讯也是非常容易的。 Erlang 系统中,我 在 们称每一个执行的线程为 Process(注意这里的特殊性,不要与其他系统中的进程相混淆)。 (注意:专有名词“Process”经常用于当执行的线程不与别的线程进行数据共享的情况对这种线程的称呼。 当这些线程之间共享数据时,我们一般把它们看作一个整体,作为一个 Process 进程。在 Erlang 中,我们 往往称呼不共享数据的 Thread 为 Process,并且很多时候是混合着叫的,读者应该自己从上下文中进行分 辨) Erlang 的内建函数 spawn 被用来创建一个新的进程:spawn(Module,Exported_Function,List of Arguments)。考虑下面的例子: -module(tut14). -export([start/0, say_something/2]). say_something(What, 0) -> done; say_something(What, Times) -> io:format("~p~n", [What]), say_something(What, Times - 1). start() -> spawn(tut14, say_something, [hello, 3]), spawn(tut14, say_something, [goodbye, 3]). 5> c(tut14). {ok,tut14} 6> tut14:say_something(hello, 3). hello hello hello done 我们可以看到函数 say_something 的第一个参数表示要“说的话”,第二个参数表示说话的次数。现在我们来 看看 start 函数,它首先启动两个 Erlang 进程(注意:这里的进程和操作系统的进程并不是一回事,有很大 的差别,具体的内容我们会在后面的内容中进行介绍),一个负责输出“hello”3 次,另一个负责输出 “goodbye”3 次。 这些进程都是用函数 sya_something。 注意这个被 spawn 函数所使用的函数必须从模块中暴 露出来,也就是说必须在模块中使用了-export 语句暴露的函数。 9> tut14:start(). hello goodbye <0.63.0>
  • 19. hello goodbye hello goodbye 注意这里并不是首先输出“hello”三次,然后再输出“googdbye”三次,而是交替出现的。这里的<0.63.0>(不 同的运行环境和机器都会有不同的具体数值哈)从何而来?这个函数返回值当然是最后一件“事情”的返回值。 在 start 函数中的最后一件事情是: spawn(tut14, say_something, [goodbye, 3]). 函数 spawn 返回一个进程标识符(也就是耳熟能详的 PID),用来唯一表示一个进程的。所以<0.63.0>是 spawn 函数被调用后返回的 pid。我们将在下面的例子中使用到 pid。 同样注意到我们这里在函数 io:format 中使用了“~p”代替“~w”。“~p”大体上和“~w”输出是一致的,但是会将过 长的可打印的词组切分为多行,并且明显的缩进每行。这也是将可打印字符作为字符串输出的常见方法。 2.2 消息传递 下面的例子中,我们创建了两个进程,其中一个重复向另一个发送消息。 -module(tut15). -export([start/0, ping/2, pong/0]). ping(0, Pong_PID) -> Pong_PID ! finished, io:format("ping finished~n", []); ping(N, Pong_PID) -> Pong_PID ! {ping, self()}, receive pong -> io:format("Ping received pong~n", []) end, ping(N - 1, Pong_PID). pong() -> receive finished -> io:format("Pong finished~n", []); {ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong, pong() end. start() -> Pong_PID = spawn(tut15, pong, []), spawn(tut15, ping, [3, Pong_PID]). 1> c(tut15). {ok,tut15} 2> tut15: start(). <0.36.0> Pong received ping Ping received pong Pong received ping Ping received pong Pong received ping
  • 20. Ping received pong ping finished Pong finished 函数 start 首先创建了一个进程,我们叫它做“pong”: Pong_PID = spawn(tut15, pong, []) 这个进程执行 tut15:pong()。Pong_PID 是这个进程“pong”的标识符。函数 start 现在要创建另一个进程“ping” 了。 spawn(tut15, ping, [3, Pong_PID]), 这个进程执行: tut15:ping(3, Pong_PID) <0.36.0> 是函数 start 的返回值。 进程“pong”现在做: receive finished -> io:format("Pong finished~n", []); {ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong, pong() end. receive 关键词被用来让进程等待从其他进程发来的消息,格式如下: receive pattern1 -> actions1; pattern2 -> actions2; .... patternN actionsN end. 注意:在 end 之前没有“;”。 在 Erlang 进程之间传递的消息都是简单的合法的 Erlang“短语(Term)”。 可以是列表、 元组、 整数、 常量或者 pid 什么的。 每个进程都有自己的输入消息队列,用以接受消息。新的消息到达该进程时被放在队列的末尾。当一个进程 执行一个 receive,队列中的第一个消息被 receive 中的第一个模式(pattern)匹配测试,如果匹配,该消 息从消息队列中删除,并且执行对应 pattern 下的操作。 如此,如果第一个 pattern 没有被匹配成功,第二个模式就将被测试,如果匹配成功,该消息就会从消息队 列中删除,并且执行对应 pattern 下的操作。 如果第二个 pattern 仍然不能被测试为真,则该过程以此类推, 直到没有 pattern 可供测试为止。 如果没有 pattern 可供测试了,第一个消息将被保存在在消息队列中,并且 开始第二个消息的匹配测试工作,如果这时第二个消息匹配成功了,则删除消息队列中的第二个消息,但 是第一个消息和其他消息的状态并不受到任何影响。 如果第二个消息还是匹配失败,则该过程持续下去,依 次进行第三个、第四个消息的匹配。如果我们到了队列的末尾,该进程被阻塞(停止执行),并且等待新消 息的到来,然后重新开始匹配的过程。 当然,Erlang 的实现是非常“聪明”的,并且能够最小化每个消息被接收方的 receive 测试的次数。 现在回到我们的 ping pong 例子。 "Pong" 等待着消息。 如果常量 finished 被接收到,“pong”将输出“Pong finished”,然后继续“无所事事”。 如果 它接收一个消息是下面的格式:
  • 21. {ping, Ping_PID} 它输出“Pong received ping”,并且发送常量 pong 到进程“ping”: Ping_PID ! pong 注意:操作符“!”怎样被用来发送消息的,下面是“!”的语法: Pid ! Message 消息(可以是任何的 Erlang 的 Term)被用来向 Pid 标识的的进程发送消息。 在 pong 发送消息到进程“ping”后,“pong”再次调用了 pong 函数,这就让它回到了等待接受消息的那种状态, 从而等待其他消息的到来。现在我们来看进程“ping”。回忆它是怎么开始运行的: tut15:ping(3, Pong_PID) 看看 ping/2,我们看到 ping/2 的第二个子句被执行,并且带有参数 3(不是 0)(第一个子句是 ping(0,Pong_PID),第二个子句是 ping(N,Pong_PID),这里的 N 会被赋值为 3)。 第二个子句发送到消息到“pong”: Pong_PID ! {ping, self()}, self() 返回当前执行 self()进程的 pid,在这里就是“ping”的 pid。(回忆“pong”的代码,看看里面 Pong_PID 的情况) "Ping"现在等待着从“pong”传回的信息: receive pong -> io:format("Ping received pong~n", []) end, 并且当回复的消息到达时会输出“Ping received pong”,之后“ping”会再次调用 ping 函数。 ping(N - 1, Pong_PID) N-1 使得第一个参数递减,直到减到 0 为止。当减到 0 时,第一个子句 ping/2 会被执行: ping(0, Pong_PID) -> Pong_PID ! finished, io:format("ping finished~n", []); 常量 finished 被发送给“pong”(这将导致接受方终止)并且输出“ping finished”。 “ping”然后自己结束掉自己。 2.3 注册进程名称 在上面的例子中,我们首先创建了“pong”,然后在启动“ping”之前给“pong”一个唯一标识符。某些时候“ping” 必须直到“pong”的标识符才能够发送消息给它。某些时候进程需要知道对方的情况,但是进程之间又是完全 独立的。所以为了解决分配 pid 和互相识别的困难,Erlang 提供了一个赋予进程以名字的机制,我们能够通 过使用进程名字来代替 pid 的使用。这必须使用到内建函数 register: register(some_atom, Pid) 我们现在重写我们的 ping pong 例子,我们将赋予 ping 和 pong 进程以名字: -module(tut16). -export([start/0, ping/1, pong/0]). ping(0) -> pong ! finished, io:format("ping finished~n", []); ping(N) -> pong ! {ping, self()}, receive pong -> io:format("Ping received pong~n", []) end, ping(N - 1). pong() ->
  • 22. receive finished -> io:format("Pong finished~n", []); {ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong, pong() end. start() -> register(pong, spawn(tut16, pong, [])), spawn(tut16, ping, [3]). 2> c(tut16). {ok, tut16} 3> tut16:start(). <0.38.0> Pong received ping Ping received pong Pong received ping Ping received pong Pong received ping Ping received pong ping finished Pong finished 在函数 start/0 中, register(pong, spawn(tut16, pong, [])), 将名称“pong”赋予了进程“pong”。在进程“ping”中我们可以直接使用 pong 来对它发送消息: pong ! {ping, self()}, 所以这里的 ping/2 可以简化为 ping/1,不再使用参数 Pong_PID。 2.4 分布式编程 现在我们来重写 ping pong 例子,其中 ping 和 pong 都在不同的计算机上。在我们做这件事情之前,这里有 一些事情需要首先做到。分布式 Erlang 的实现提供了自有的安全机制来预防未经授权的 Erlang 系统访问 。 Erlang 系统与别的机器进行交互时必须有同样的 magic cookie(魔法甜饼)。 最简单的实现方法就是建立一 个.erlang.cookie 文件在 home 目录,并且复制到其他所有打算运行 Erlang 系统的机器上(如果是用的是 Windows 系统,那么就是环境变量$HOME 所指的目录,可能需要我们手动进行设定),在 UNIX 或者 LINUX 系统下可以安全的忽略上面的东西并且只需要简单的创建一个.erlang.cookie 文件就可以了。该文件 需要包含一行常量,例如在 LINUX 系统上,我们可以这样设定: $ cd $ cat > .erlang.cookie this_is_very_secret $ chmod 400 .erlang.cookie 这个 chmod 命令让.erlang.cookie 的文件访问许可只能限定于文件的所有者,这是必须的。 当我们启动一个 Erlang 系统并且试图与别的 Erlang 系统进行交互时,我们必须给定名称: erl -sname my_name 我们稍后将看到更多的细节。如果你打算尝试分布式 Erlang,但是只有一台计算机,你可以同时打开两个 Erlang 环境在同一台计算机上,然后赋予他们不同的名称就可以了(使用上面的操作系统 shell 下的命令)。 每个运行在计算机上的 Erlang 系统都被称为一个节点(Node)。
  • 23. (注意:erl -sname 命令假定所有节点都在同一个 IP 域内,如果我们需要在不同的域内使用,就用-name 参数代替原有的参数,但是必须给出所有的 IP 地址) 这里是修改后分别运行在不同节点上的 ping pong 例子: -module(tut17). -export([start_ping/1, start_pong/0, ping/2, pong/0]). ping(0, Pong_Node) -> {pong, Pong_Node} ! finished, io:format("ping finished~n", []); ping(N, Pong_Node) -> {pong, Pong_Node} ! {ping, self()}, receive pong -> io:format("Ping received pong~n", []) end, ping(N - 1, Pong_Node). pong() -> receive finished -> io:format("Pong finished~n", []); {ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong, pong() end. start_pong() -> register(pong, spawn(tut17, pong, [])). start_ping(Pong_Node) -> spawn(tut17, ping, [3, Pong_Node]). 让我们假设我们有两台计算机分别叫做 gollum 和 kosken。我们首先打开在计算机 kosken 上打开一个节点, 称为 ping,然后打开 gollum,叫做 pong。 在 kosken 上(在一台 Linux 系统上): kosken> erl -sname ping Erlang (BEAM) emulator version 5.2.3.7 [hipe] [threads:0] Eshell V5.2.3.7 (abort with ^G) (ping@kosken)1> 在 gollum 上(在一台 Windows 系统上) : gollum> erl -sname pong Erlang (BEAM) emulator version 5.2.3.7 [hipe] [threads:0] Eshell V5.2.3.7 (abort with ^G) (pong@gollum)1> 现在我们在 gollum 上开始“pong”进程: (pong@gollum)1> tut17:start_pong(). true 然后再 kosken 上开始“ping”进程(从上面的代码中我们可以看到 start_ping 函数的一个参数是 pong 所运行 的 Erlang 系统的名称): (ping@kosken)1> tut17:start_ping(pong @gollum). <0.37.0>
  • 24. Ping received pong Ping received pong Ping received pong ping finished 这里我们可以看到 ping pong 例子运行起来了,在“pong”一端,我们可以看到: (pong@gollum)2> Pong received ping Pong received ping Pong received ping Pong finished (pong@gollum)2> 看看 tut17 中的代码,我们研究一下 pong 函数,发现并没有和前面的例子有什么改变: {ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong, 这里的工作与运行“ping”的节点完全无关。Erlang 的 pid 包含了关于进程在何处执行的信息,所以如果你知 道一个进程的 pid,操作符“!”就可以被用来向这个进程传递消息,而不论该进程是否是在本机还是在远程。 一个不同点在于,如果是在另一个节点上的有名称的进程,我们对其发送消息: {pong, Pong_Node} ! {ping, self()}, 我们使用元组{regiestered_name,node_name}替换只是使用 registered_name。 在之前的例子,我们开始“ping”和“pong”与不同的节点。 spawn 可以被用来启动其他节点上的进程。 下面的例 子还是 ping pong 程序,不过我们这次将“远程”启动进程: -module(tut18). -export([start/1, ping/2, pong/0]). ping(0, Pong_Node) -> {pong, Pong_Node} ! finished, io:format("ping finished~n", []); ping(N, Pong_Node) -> {pong, Pong_Node} ! {ping, self()}, receive pong -> io:format("Ping received pong~n", []) end, ping(N - 1, Pong_Node). pong() -> receive finished -> io:format("Pong finished~n", []); {ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong, pong() end. start(Ping_Node) -> register(pong, spawn(tut18, pong, [])), spawn(Ping_Node, tut18, ping, [3, node()]). 假定一个 Erlang 系统叫做“ping”(不是“ping”进程哈)已经在 kosken 上启动了,那么我们在 gollum 上操作:
  • 25. (pong@gollum)1> tut18:start(ping@kosken). <3934.39.0> Pong received ping Ping received pong Pong received ping Ping received pong Pong received ping Ping received pong Pong finished ping finished 注意这里所有的输出都在 gollum 上。这是因为 io 系统会找寻进程创建的位置,并在该位置进行输出。