erlang jiacheng

23
Erlang 编 编编 (一) 1 编编编编 1.1 The Erlang Shell 大大大大 统一一 shell UNIX 大 LINUX 大大大大大 Windows 大大大大大大大大大大 Erlang 大大大大大大大 shell 大大大大大大大大 大大大大大大 大大大大大大大 ,。 Erlang 大 shell 大 大大大大大大 ,一% 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

Upload: air-smile

Post on 28-Jun-2015

1.227 views

Category:

Documents


2 download

TRANSCRIPT

Page 1: Erlang jiacheng

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 中键入下面的内容:

Page 2: Erlang jiacheng

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).

Page 3: Erlang jiacheng

-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

Page 4: Erlang jiacheng

消息。上面的输出看上去是“典型的一团糟”,但是经过我们认真的观察,我们可以清楚的直到代码中到底是发生了什么。 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.

Page 5: Erlang jiacheng

[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、E1、E2、R、A、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部分的长度的和。 (对于高级一点的读者:这里并非只能使用递归的方式,也有一些更好的方式来实现哈。)

Page 6: Erlang jiacheng

一般情况下我们可以在我们在其他语言中使用“记录 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 一个大一些的例子

Page 7: Erlang jiacheng

现在有一个大一些的例子帮助我们更加深入的学习。这里我们准备了一个从一组城市中读取气温的列表。其中一些是摄氏度,另一些是华氏度的表示,让我们以更加适于阅读的方式输出这些信息:%% 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 变量的匹配、限定和作用域我们可能需要寻找在列表中的最高和最低温度。在我们动手扩展我们前面的程序之前,让我们来看一个在列表中寻找最大值的元素的函数:

Page 8: Erlang jiacheng

-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}}.

Page 9: Erlang jiacheng

{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]) =>

Page 10: Erlang jiacheng

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}}, {cape_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}},

Page 11: Erlang jiacheng

[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}}, {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

我们现在已经添加了一个函数去寻找有最高气温和最低气温的城市了。这个程序并不是最有效率的,我们 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}}) ->

Page 12: Erlang jiacheng

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}}, {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

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 从最高处开始执行,直到它找到一个为真的条件,并执行其内部的代码,并且很重要的是它将忽略其他剩下的条件,不论其他剩下的条件中是否还有为真的情况。一个条件当是常量的时候意味着

Page 13: Erlang jiacheng

永远为真,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}]} **

Page 14: Erlang jiacheng

注意到执行 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;

Page 15: Erlang jiacheng

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).

Page 16: Erlang jiacheng

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]

Page 17: Erlang jiacheng

现在让我们输出城市的温度列表: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([{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)。所以在 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}

Page 18: Erlang jiacheng

100> 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,

这里我们引入了一个概念——匿名变量"_"(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次,另一个负责

Page 19: Erlang jiacheng

输出“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}

Page 20: Erlang jiacheng

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 可供测试了,第一个消息将被保存在在消息队列中,并且开始第二个消息的匹配测试工作,如果这时第二个消息匹配成功了,则删除消息队列中的第二

Page 21: Erlang jiacheng

个消息,但是第一个消息和其他消息的状态并不受到任何影响。如果第二个消息还是匹配失败,则该过程持续下去,依次进行第三个、第四个消息的匹配。如果我们到了队列的末尾,该进程被阻塞(停止执行),并且等待新消息的到来,然后重新开始匹配的过程。 当然,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", []);

Page 22: Erlang jiacheng

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

Page 23: Erlang jiacheng

$ 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>

Page 24: Erlang jiacheng

现在我们在 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} ->

Page 25: Erlang jiacheng

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 系统会找寻进程创建的位置,并在该位置进行输出。