您的位置:首页 > 编程语言

Erlang 编程参考手册(全)

2009-03-24 17:14 176 查看
http://blovedot.javaeye.com/blog/147714

 

 关键字: erlang 编程 参考手册 全

Erlang 编程参考手册 (第一部分,省略没有太多信息含量的第一章节)

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、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部分的长度的和。

(对于高级一点的读者:这里并非只能使用递归的方式,也有一些更好的方式来实现哈。)

一般情况下我们可以在我们在其他语言中使用“记录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}}, {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}},

    [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}}) ->

    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

2 数据类型

2.1 项Term

Erlang提供了一组数据类型,我们将在本章节中逐个认识。某种数据类型的一个实例称为一个项。

2.2 数值Number

这里有两种数值类型,整型和浮点型。除了一些常见的转换外,Erlang还有两种特殊的转换形式:

$char

获取字符的ASCII码。

base#value

base进制(进制的范围为2到36的整数)。在Erlang 5.2/OTP R9B和早前的版本中,这个范围较小,只能是2到16.

例子:

1> 42.

42

2> $A.

65

3> $/n.

10

4> 2#101.

5

5> 16#1f.

31

6> 2.3.

2.30000

7> 2.3e3.

2300.00

8> 2.3e-3.

2.30000e-3

   

2.3 常量Atom

一个常量可以看作一个词条,而且该词条就是它的名称。一个常量如果不是以小写字符开始或者包含其他字符(例如数字、_、@等)则应该是使用单引号包起来。

例子:

hello

phone_number

'Monday'

'phone number'

   

2.4 比特式Binary

一个比特式用来存储没有类型的内存数据。

比特式被表示成为比特的直接形式。

例子:

1> <<10,20>>.

<<10,20>>

2> <<"ABC">>.

<<65,66,67>>

   

更多的例子可以稍等一段时间,会有编程例子放出。

2.5 引用Reference

一个引用是在Erlang运行时系统中的一个唯一的项,通过make_ref/0创建。

2.6 函数项Fun

一个函数项是一个函数化对象。它可以创建一个匿名函数,并且传递函数自身,作为另一个函数的参数。

例子:

1> Fun1 = fun (X) -> X+1 end.

#Fun<erl_eval.6.39074546>

2> Fun1(2).

3

   

在编程例子中会有更多的Fun表达式例子,敬请期待。

2.7 端口标识符Port Identifier

一个端口标识符就是一个Erlang端口。open_port/2,被用来创建端口,将返回该类型的一个值。

2.8 进程ID Pid

一个进程标识符pid,spawn/1,2,3,4,spawn_link/1,2,3,4,和spawn_opt/4,都可以用来创建进程,返回一个该类型的值。例子:

1> spawn(m, f, []).

<0.51.0>

   

内置函数 self() 返回调用进程的pid,例子:

-module(m).

-export([loop/0]).

loop() ->

    receive

        who_are_you ->

            io:format("I am ~p~n", [self()]),

            loop()

    end.

1> P = spawn(m, loop, []).

<0.58.0>

2> P ! who_are_you.

I am <0.58.0>

who_are_you

2.9 元组Tuple

带有确定数量的项的复合数据类型:

{Term1,...,TermN}

   

其中每个项都被称为元素。元素的数量就是该元组的大小。

有很多操作元组的函数。

例子:

1> P = {adam,24,{july,29}}.

{adam,24,{july,29}}

2> element(1,P).

adam

3> element(3,P).

{july,29}

4> P2 = setelement(2,P,25).

{adam,25,{july,29}}

5> size(P).

3

6> size({}).

0

   

2.10 列表List

带有不定长项的复合数据类型。

[Term1,...,TermN]

   

其中的每一项都称为一个元素。元素的数量就是这个列表的长度。

形式上,一个列表可以是空列表[]或者是一个有头元素和尾元素的列表。可以使用[H|T]对列表进行划分。列表[Term1,...,TermN]其实可以被表示为[Term1|[...|[TermM|[]]]。

例子:

[] is a list, thus

[c|[]] is a list, thus

[b|[c|[]]] is a list, thus

[a|[b|[c|[]]]] is a list, or in short [a,b,c].

尾部是一个列表的列表有时候称为proper list严格列表。当然,尾部不是列表的列表,例如[a|b]也是合法的。在实际中,我们使用单列表可能更多一点。

例子:

1> L1 = [a,2,{c,4}].

[a,2,{c,4}]

2> [H|T] = L1.

[a,2,{c,4}]

3> H.

a

4> T.

[2,{c,4}]

5> L2 = [d|T].

[d,2,{c,4}]

6> length(L1).

3

7> length([]).

0

   

一组操作列表的函数可以在STDLIB中的模块lists中找到。

2.11 字符串String

字符串使用双引号引起,但是并不是一个单独的Erlang数据类型。Erlang系统内部是使用[$h,$e,$l,$l,$o]来表示字符串"hello"的,也就是[104,101,108,108,111]。

两个临近的字符串可以被链接为一个字符串。这是在编译时完成的,而非运行时。例子:

"string" "42"

   

等效于:

"string42"

   

2.12 记录Record

一个记录是一个数据结构,用来存储定长的元素组。它有一个命名域,这一点与C语言相同。尽管如此,记录并不是一个真正意义上的数据类型。记录被Erlang表示为元组表达式,这也是在编译时完成的。因此,记录表达式并没有被Erlang所真正“认识”,除了某些特殊的操作。

例子:

-module(person).

-export([new/2]).

-record(person, {name, age}).

new(Name, Age) ->

    #person{name=Name, age=Age}.

1> person:new(ernie, 44).

{person,ernie,44}

2.13 布尔Boolean

在Erlang中没有布尔值。而是使用常量true和false来表示。

例子:

1> 2=<3.

true

2> true or false.

true

   

2.14 转义字符Escape Sequences

下面是一些可以被使用的转义字符:

Recognized Escape Sequences.

描述

/b

backspace

/d

delete

/e

escape

/f

form feed

/n

newline

/r

carriage return

/s

space

/t

tab

/v

vertical tab

/XYZ, /YZ, /Z

character with octal representation XYZ, YZ or Z

/^a.../^z

/^A.../^Z

control A to control Z

/'

single quote

/"

double quote

//

backslash

2.15 类型转换Type Conversions

下面是一些用来进行类型转换的内置函数:

1> atom_to_list(hello).

"hello"

2> list_to_atom("hello").

hello

3> binary_to_list(<<"hello">>).

"hello"

4> binary_to_list(<<104,101,108,108,111>>).

"hello"

5> list_to_binary("hello").

<<104,101,108,108,111>>

6> float_to_list(7.0).

"7.00000000000000000000e+00"

7> list_to_float("7.000e+00").

7.00000

8> integer_to_list(77).

"77"

9> list_to_integer("77").

77

10> tuple_to_list({a,b,c}).

[a,b,c]

11> list_to_tuple([a,b,c]).

{a,b,c}

12> term_to_binary({a,b,c}).

<<131,104,3,100,0,1,97,100,0,1,98,100,0,1,99>>

13> binary_to_term(<<131,104,3,100,0,1,97,100,0,1,98,100,0,1,99>>).

{a,b,c}

3 匹配 Pattern Matching

3.1 匹配 Pattern Matching

变量通过匹配机制进行对数据的绑定。匹配发生在函数的执行过程、case- receive- try-表达式和匹配操作符(=)表达式中。

在匹配中,左手边的“pattern模式”将于右手边的项进行匹配。如果匹配成功,则该未绑定变量变为已绑定状态。如果绑定失败,就发生一个运行时错误。

例子:

1> X.

** 1: variable 'X' is unbound **

2> X = 2.

2

3> X + 1.

3

4> {X, Y} = {1, 2}.

** exited: {{badmatch,{1,2}},...} **

5> {X, Y} = {2, 3}.

{2,3}

6> Y.

3

4 模块Modules

4.1 模块语法Module Syntax

Erlang代码被模块分割为不同的部分。一个模块通常包含一组属性和函数声明,均以(.)结尾。例如:

-module(m).          % module attribute

-export([fact/1]).   % module attribute

fact(N) when N>0 ->  % beginning of function declaration

    N * fact(N-1);   %  |

fact(0) ->           %  |

    1.               % end of function declaration

4.2 模块属性Module Attributes

一个模块属性定义了该模块的特定信息。一个属性通常包含一个标记和对应的值。

-Tag(Value).

   

Tag必须是常量,这时Value必须是字符类型的项。

任何的模块属性都可以被设定。属性是存储在编译后的代码中的,并且能够被使用。例如,函数beam)lib:chunks/2。

这里有几个模块属性是预定义了含义的,一些包含了两个参数,但是用户定义的模块属性只能有一个参数。

4.2.1 预定义模块属性Pre-Defined Module Attributes

预定义的模块属性可以被之前的任何函数声明所替换。

-module(Module).

模块声明,定义该模块的名称。模块的名称必须是一个常量,应该与该模块所在的代码文件名相同。

该属性必须首先被定义,而且也是惟一一个被强制要求的属性。

-export(Functions).

暴露函数。指定在模块中的哪些函数被向外公开可见。Functions是一个列表[Name1/Arity1,...,NameN/ArityN],这里的NameI是一个常量,二ArityI应该是一个整数。

-import(Module,Functions).

导入函数。导入的函数可以被以本地函数一样的方式进行调用,这不需要使用模块名作为前缀。

Module,是一个常量,指定导入的模块,Funcations是一个类似于export的函数列表。

-compile(Options).

编译选项。Options,一个单一选项或者是一组选项,这些选项将被在编译的时候自动添加道编译选项中。

-vsn(Vsn).

模块版本。Vsn是一个字符项,可以通过beam_lib:version/1来读取。

如果该属性没有被指定,版本默认为该模块的校验和。

4.2.2 行为模块属性Behaviour Module Attribute

我们能够通过行为(Behaviour)指定某个模块为回调模块:

-behaviour(Behaviour).

     

Behaviour 是行为的名称,可能是某个用户自定义的行为或者某个OTP标准行为 gen_server, gen_fsm, gen_event 或者 supervisor。

拼写为 behavior 系统也是接受的。

4.2.3 宏和记录的定义Macro and Record Definitions

用来定义宏和记录的方式和模块的属性的语法是一样的:

-define(Macro,Replacement).

-record(Record,Fields).

     

宏和记录的定义可以存在于模块的任何地方,甚至在函数的声明中也可以。

4.2.4 文件包含File Inclusion

和模块的其他属性的使用一样:

-include(File).

-include_lib(File).

     

File, 一个字符串,应该指向一个真是存在的文件。被指向的文件的内容将被包含在声明include的地方。

包含文件一般用在记录和宏定义处,用来供多个模块共享。推荐对这些将被包含的文件使用.hrl作为后缀名。

File 可以使用一个路径组件$VAR开始,并且返回os:getenv(VAR),如果os:getenv(VAR)返回false,则$VAR为空。

例子:

-include("my_records.hrl").

-include("incdir/my_records.hrl").

-include("/home/user/proj/my_records.hrl").

-include("$PROJ_ROOT/my_records.hrl").

     

include_lib 和 include类似, 但是并不指向绝对文件,而忽视第一个路径组件被假设为一个应用的名称。例子:

-include_lib("kernel/include/file.hrl").

     

代码中使用了 code:lib_dir(kernel) 来寻在当前版本的kernel的目录,并且在这些子目录中寻找文件file.hrl。

4.2.5 文件和行的设置Setting File and Line

类似的,这里预定义了宏FILE和LINE:

-file(File, Line).

     

该属性被一些工具(例如Yecc)获取编译的相关信息和获取源文件的相关信息。

4.3 注释Comments

注释可以放置于模块的任何地方,除了字符串和引起的常量中。注释以“%”开始,但是不包含一个结尾符也是合法的。(在每个行的末尾会有一个空白字符)

   

Erlang 编程 (第四部分)

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

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的话。

Erlang 编程(第五部分)

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~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息