第一章 Erlang 指导
我们以一个对Erlang的基本指导作为开始,目的是介绍给读者Erlang的主要特点。这里讨论的话题将不会很详细,它们将在后续章节中继续讨论。
我们用一些简单的Erlang程序作为例子开始介绍。
1.1 顺序编程
例程1.1计算一个整数的阶乘:
>math1:factorial(6).
720
>math1:factorial(25).
15511210043330985984000000
上面的“>”是shell提示符,它后面的是用户输入的表达式,下一行是这个表达式的计算结果。
以上factorial的代码如何编译以及如何被加载进Erlang系统与用户使用的操作系统有关,不在本书讨论的范围之内。
我们的例子中,factorial函数有两个定义子句:第一个子句是计算factorial(0)的规则,第二个子句是计算factorial(N)的规则。当计算factorial函数时,这两个子句会按照它们在模块(math1)中出现的顺序被扫描,直到其中一个与函数调用匹配。当匹配成功时,“->”右侧的表达式将被计算,并且在它被计算之前,其中的任何出现在函数定义中的变量会被置换为它所绑定的值。
所有Erlang函数都属于一些特别的模块。再简单不过的模块包含module声明、export声明和导出函数的代码。
导出的函数可以在模块外部运行,而其它未导出的函数则只能在模块内部运行。
例程1.2给出了这样的例子:
-module(math2).
-export([double/1]).
double(X) ->
times(X,2).
times(X,N) ->
X * N.
> math2:double(10).
20
> math2:times(5,2).
** undefined function:math2:times(5,2) **
在例程1.2中,module声明“-module(math2)”定义了这个模块的名字,export属性“-export([double/1])”表明有一个参数的函数从模块中导出。
函数调用可以嵌套:
> math2:double(math2:double(2)).
8
在Erlang中,通过模式匹配提供选择。例程1.3给出了一个例子:
-module(math3).
-export([area/1]).
area({square,Side}) ->
Side * Side;
area({rectangle,X,Y}) ->
X * Y;
area({circle,Radius}) ->
3.14159 * Radius * Radius
area({triangle,A,B,C}) ->
S = (A + B + C) / 2,
math:sqrt(S*(S-A)*(S-B)*(S-C)).
{triangle,6,7,8}
> math3:area(Thing)
20.3332
这里Thing被绑定到tuple{triangle,6,7,8}-我们说Thing的值是一个大小为4的tuple-有4个元素。第一个元素是原子triangle,剩下的三个元素分别是整数6、7、8。
1.2 数据类型
Erlang提供下面几种数据类型:
-原子(atom)。例如:abc,'An atom with space',monday,green,hello_world。它们是只是有名字的常量。
-list。例如:[],[a,b,12],[22],[a,'hello friend']。list用来存放可变数量的元素。用方括号括起来即为list。
tuple和list中的元素可以是任意Erlang支持的数据类型-由此我们可以创建任意复杂的结构。
可以用变量来保存Erlang各种数据类型的值。变量总是以大写字母开头,例如下面这段代码:
X = {book,preface,acknowledgements,contents,
{chapters,[
{chapter,1,'An Erlang Tutorial'},
{chapter,2,...}
]
}},
创建了一个复杂的数据结构,该结构被保存在变X中。
1.3 模式匹配
模式匹配可以用来一为变量赋值,二控制程序流程。Erlang是一种单一赋值语言,也就是说,当一个变量被赋值后,它的值就不能再改变了。
进行模式匹配时是用数据匹配模式的。如果一个数据和一个模式形式相同,即匹配成功。任何出现在模式中的变量会绑定数据中与之对应位置的数据结果。
1.3.1 调用函数时的模式匹配
例程1.4定义了函数convert,它可以用来在摄氏温度和华氏温度、列氏温度之间进行换算。convert的第一个参数是一个tuple,包含原始温标类型和温度,第二个参数是目的温标类型。
-module(temp).
export([convert/2]).
convert({fahrenheit,Temp},celsius) ->
{celsius,5 * (Temp - 32) / 9};
convert({celsius,Temp},fahrenheit) ->
{fahrenheit,32 + Temp * 9 / 5};
convert({reaumur,Temp},celsius) ->
{celsius,10 * Temp / 8};
convert({celsius,Temp},reaumur) ->
{reaumur,8 * Temp / 10};
convert({X,_},Y) ->
{cannot,convert,X,to,Y}.
> temp:convert({fahrenheit,98.6},celsius).
{celsius,37.0000}
> temp:convert({reaumur,80},celsius).
{celsius,100.0000}
> temp:convert({reaumur,80},fahrenheit).
{cannot,convert,reaumur,to,fahrenheit}
1.3.2 匹配工具"="
Pattern = Expression中,先计算Expression,再将其结果与Pattern进行匹配。匹配可能成功,也可能失败。如果成功,任何出现在Pattern中的变量都会被绑定,例如:
> N = {12,banana}.
{12,banana}
> {A,B} = N.
{12,banana}
> A.
12
> B.
banana
"="可以从一个复杂的数据结构中将某个元素解离出来,例如:
> {A,B} = {[1,2,3],{x,y}}.
{[1,2,3],{x,y}}
> A.
[1,2,3]
> B.
{x,y}
> [a,X,b,Y] = [a,{hello,fred},b,1].
[a,{hello,fred},b,1]
> X.
{hello,fred}
> Y.
1
> {_,L,_} = {fred,{likes,[wine,women,song]},
{drinks,[whisky,beer]}}.
{fred,{likes,[wine,women,song]},{drinks,[whisky,beer]}}
> L.
{likes,[wine,women,song]}
上面表达式中出现的下划线是一个匿名变量或不必关心的变量。当语法要求某个位置需要一个变量,但我们不关心它的取值如何,此时就可以将下划线这个特殊变量放到那个位置(充当占位符)。
如果匹配成功,表达式Lhs = Rhs的值定义为Rhs。因此,我们可将多个匹配写到一个表达式中,例如:
{A,B} = {X,Y} = C = g(a,12)
"="是infix right associative operator。于是,A = B = C = D相当于A = (B = (C = D))。
(注:Lhs是Left hand side的简写,Rhs是Right hand side的简写)
1.4 内建函数(Built-In Function)
用Erlang编写程序完成一些操作要么不可能要么效率太低。例如,找不出什么办法可以获知一个atom的内部结构,或者今天的日期等。这些都不是语言本身力所能及的。因此,Erlang提供了一些内建函数来完成这些工作。
例如函数atom_to_list/1将一个atom转换为由其对应字母ASCII码值构成的list;函数date/0返回当前日期。
> atom_to_list(abc).
[97,98,99]
> date().
{93,1,10}
在附录B中将会给出一个所有内建函数的完整列表。
1.5 并发
Erlang是一种并发编程语言,这意味着可以用Erlang直接编写并行的程序。这种并行能力是由Erlang提供的,而非操作系统。
为了控制一组并行进程,Erlang提供了三种工具:函数spawn启动一个并行的计算(也叫进程);send(!)向一个进程发送消息;receive从一个进程接收消息。
函数spawn/3启动一个并行进程并返回该进程的PID,利用这个PID可以向该进程发送消息,也可以从它那里接收消息。
Pid ! Msg表示向一个进程发送消息。其中Pid可以是一个表达式或常量,但其计算结果都必须为进程PID;Msg是发送给进程(Pid指向的进程)的消息,例如:
Pid ! {a,12}
将消息{a,12}发送给Pid(Process identifier的简写)指向的进程。在发送消息之前,会先计算“!”两边的表达式,因此
foo(12) ! math3:area({square,5})
会计算函数foo(12)(其返回值必须为有效的进程PID)和math3:area({square,5})然后将后者的结果(25)作为消息发送给前者指向的进程。然而它们的计算先后次序是不确定的。
receive用来接收消息,它的语法如下:
receive
Message1 ->
... ;
Message2 ->
... ;
...
end
这种形式表示尝试着接收一个消息,它可能和Message1、Message2、...中的一个匹配。进程计算receive时会先挂起(即什么也不做),当接收到一个消息而它又和Message1、Message2、...中的一个匹配时,就会计算相应“->”后面的代码。
当接收到一个消息时,任何出现在消息接收模式中未绑定的变量都会被绑定到消息中与之对应的值上。
receive的返回值是匹配成功的子句的值。
我们知道send(!)是发送消息,receive是接收消息。而更准确的说法是,send(!)将消息发送到目的进程的邮箱(mailbox)中,receive则是试着从当前进程的邮箱中删除消息。
receive是有选择的。它从等待进程处理的消息队列中取出第一个与消息模式匹配成功的消息。如果receive的消息模式没有一个匹配上,进程就会挂起,直到接收一个能匹配的消息,未匹配的消息会被保存留待以后处理。
1.5.1 一个echo进程
我们将创建一个echo进程作为并行处理的简单例子。这个进程显示出任何发给它的消息。我们假设进程A发送{A,Msg}给echo进程,于是echo进程发送给进程A一个包含Msg的新消息。示意图表示如下:
例如1.5中,echo:start()创建了一个简单echo进程,它返回发送给它的消息。
-module(echo).
-export([start/0,loop/0]).
start() ->
spawn(echo,loop,[]).
loop() ->
receive
{From,Message} ->
From ! Message,
loop()
end.
...
Id = echo:start(),
Id ! {self(),hello}
...
启动一个并行进程并向该进程发送消息{self(),hello}。其中self()是一个内建函数,它返回当前进程的PID。
我们以一个对Erlang的基本指导作为开始,目的是介绍给读者Erlang的主要特点。这里讨论的话题将不会很详细,它们将在后续章节中继续讨论。
我们用一些简单的Erlang程序作为例子开始介绍。
1.1 顺序编程
例程1.1计算一个整数的阶乘:
-module(math1).
-export([factorial/1]).
factorial(0) -> 1;
factorial(N) -> N * factorial(N-1).
-export([factorial/1]).
factorial(0) -> 1;
factorial(N) -> N * factorial(N-1).
例程1.1
函数可以被一个叫做shell(erl shell)的程序交互的计算。shell提示用户输入表达式,然后计算该表达式,并打印结果,例如:
>math1:factorial(6).
720
>math1:factorial(25).
15511210043330985984000000
上面的“>”是shell提示符,它后面的是用户输入的表达式,下一行是这个表达式的计算结果。
以上factorial的代码如何编译以及如何被加载进Erlang系统与用户使用的操作系统有关,不在本书讨论的范围之内。
我们的例子中,factorial函数有两个定义子句:第一个子句是计算factorial(0)的规则,第二个子句是计算factorial(N)的规则。当计算factorial函数时,这两个子句会按照它们在模块(math1)中出现的顺序被扫描,直到其中一个与函数调用匹配。当匹配成功时,“->”右侧的表达式将被计算,并且在它被计算之前,其中的任何出现在函数定义中的变量会被置换为它所绑定的值。
所有Erlang函数都属于一些特别的模块。再简单不过的模块包含module声明、export声明和导出函数的代码。
导出的函数可以在模块外部运行,而其它未导出的函数则只能在模块内部运行。
例程1.2给出了这样的例子:
-module(math2).
-export([double/1]).
double(X) ->
times(X,2).
times(X,N) ->
X * N.
例程1.2
函数double/1(注:意思是函数double有一个参数)能在模块外部计算,而times/2完全是局部函数,例如:
> math2:double(10).
20
> math2:times(5,2).
** undefined function:math2:times(5,2) **
在例程1.2中,module声明“-module(math2)”定义了这个模块的名字,export属性“-export([double/1])”表明有一个参数的函数从模块中导出。
函数调用可以嵌套:
> math2:double(math2:double(2)).
8
在Erlang中,通过模式匹配提供选择。例程1.3给出了一个例子:
-module(math3).
-export([area/1]).
area({square,Side}) ->
Side * Side;
area({rectangle,X,Y}) ->
X * Y;
area({circle,Radius}) ->
3.14159 * Radius * Radius
area({triangle,A,B,C}) ->
S = (A + B + C) / 2,
math:sqrt(S*(S-A)*(S-B)*(S-C)).
例程1.3
计算math3:area({triangle,3,4,5})得到6.0000,计算math3:area({square,5})得到25,正如我们所期望的那样。例程1.3引人了几个新的概念:
- tuples-place holder for complex data structures.我们可以用下面与erl shell的对话来说明这一点:
{triangle,6,7,8}
> math3:area(Thing)
20.3332
这里Thing被绑定到tuple{triangle,6,7,8}-我们说Thing的值是一个大小为4的tuple-有4个元素。第一个元素是原子triangle,剩下的三个元素分别是整数6、7、8。
- 模式匹配-用来在函数中进行子句选择。函数area/1用4个子句来定义。调用math3:area({circle,10})的结果是Erlang系统尝试用tuple{circle,10}匹配定义area/1的子句中的一个。在我们的例子中,第三个子句显示将被匹配,而出现在函数定义头部的空闲变量Radius被绑定到函数调用中提供的值(这里是10)。
- 序列和临时变量-它们是在最后一个子句中引入的。最后一个子句的子句体是一个由两个语句构成的序列,它们由逗号隔开,并顺序计算。子句的返回值定义为计算子句最后一个语句得到的值。在area/1最后一个子句的第一个语句中,我们引入了一个临时变量S。
1.2 数据类型
Erlang提供下面几种数据类型:
- 常量数据类型-它们是不能被分割为更多基本类型的数据类型:
-原子(atom)。例如:abc,'An atom with space',monday,green,hello_world。它们是只是有名字的常量。
- 复合数据类型-用来将其它数据类型组合在一起。Erlang中有两种复合数据类型:
-list。例如:[],[a,b,12],[22],[a,'hello friend']。list用来存放可变数量的元素。用方括号括起来即为list。
tuple和list中的元素可以是任意Erlang支持的数据类型-由此我们可以创建任意复杂的结构。
可以用变量来保存Erlang各种数据类型的值。变量总是以大写字母开头,例如下面这段代码:
X = {book,preface,acknowledgements,contents,
{chapters,[
{chapter,1,'An Erlang Tutorial'},
{chapter,2,...}
]
}},
创建了一个复杂的数据结构,该结构被保存在变X中。
1.3 模式匹配
模式匹配可以用来一为变量赋值,二控制程序流程。Erlang是一种单一赋值语言,也就是说,当一个变量被赋值后,它的值就不能再改变了。
进行模式匹配时是用数据匹配模式的。如果一个数据和一个模式形式相同,即匹配成功。任何出现在模式中的变量会绑定数据中与之对应位置的数据结果。
1.3.1 调用函数时的模式匹配
例程1.4定义了函数convert,它可以用来在摄氏温度和华氏温度、列氏温度之间进行换算。convert的第一个参数是一个tuple,包含原始温标类型和温度,第二个参数是目的温标类型。
-module(temp).
export([convert/2]).
convert({fahrenheit,Temp},celsius) ->
{celsius,5 * (Temp - 32) / 9};
convert({celsius,Temp},fahrenheit) ->
{fahrenheit,32 + Temp * 9 / 5};
convert({reaumur,Temp},celsius) ->
{celsius,10 * Temp / 8};
convert({celsius,Temp},reaumur) ->
{reaumur,8 * Temp / 10};
convert({X,_},Y) ->
{cannot,convert,X,to,Y}.
例程1.4
计算函数convert时,函数调用中的参数会与函数定义中的参数模式进行匹配,一旦匹配成功,就会计算"->"后面的代码:
> temp:convert({fahrenheit,98.6},celsius).
{celsius,37.0000}
> temp:convert({reaumur,80},celsius).
{celsius,100.0000}
> temp:convert({reaumur,80},fahrenheit).
{cannot,convert,reaumur,to,fahrenheit}
1.3.2 匹配工具"="
Pattern = Expression中,先计算Expression,再将其结果与Pattern进行匹配。匹配可能成功,也可能失败。如果成功,任何出现在Pattern中的变量都会被绑定,例如:
> N = {12,banana}.
{12,banana}
> {A,B} = N.
{12,banana}
> A.
12
> B.
banana
"="可以从一个复杂的数据结构中将某个元素解离出来,例如:
> {A,B} = {[1,2,3],{x,y}}.
{[1,2,3],{x,y}}
> A.
[1,2,3]
> B.
{x,y}
> [a,X,b,Y] = [a,{hello,fred},b,1].
[a,{hello,fred},b,1]
> X.
{hello,fred}
> Y.
1
> {_,L,_} = {fred,{likes,[wine,women,song]},
{drinks,[whisky,beer]}}.
{fred,{likes,[wine,women,song]},{drinks,[whisky,beer]}}
> L.
{likes,[wine,women,song]}
上面表达式中出现的下划线是一个匿名变量或不必关心的变量。当语法要求某个位置需要一个变量,但我们不关心它的取值如何,此时就可以将下划线这个特殊变量放到那个位置(充当占位符)。
如果匹配成功,表达式Lhs = Rhs的值定义为Rhs。因此,我们可将多个匹配写到一个表达式中,例如:
{A,B} = {X,Y} = C = g(a,12)
"="是infix right associative operator。于是,A = B = C = D相当于A = (B = (C = D))。
(注:Lhs是Left hand side的简写,Rhs是Right hand side的简写)
1.4 内建函数(Built-In Function)
用Erlang编写程序完成一些操作要么不可能要么效率太低。例如,找不出什么办法可以获知一个atom的内部结构,或者今天的日期等。这些都不是语言本身力所能及的。因此,Erlang提供了一些内建函数来完成这些工作。
例如函数atom_to_list/1将一个atom转换为由其对应字母ASCII码值构成的list;函数date/0返回当前日期。
> atom_to_list(abc).
[97,98,99]
> date().
{93,1,10}
在附录B中将会给出一个所有内建函数的完整列表。
1.5 并发
Erlang是一种并发编程语言,这意味着可以用Erlang直接编写并行的程序。这种并行能力是由Erlang提供的,而非操作系统。
为了控制一组并行进程,Erlang提供了三种工具:函数spawn启动一个并行的计算(也叫进程);send(!)向一个进程发送消息;receive从一个进程接收消息。
函数spawn/3启动一个并行进程并返回该进程的PID,利用这个PID可以向该进程发送消息,也可以从它那里接收消息。
Pid ! Msg表示向一个进程发送消息。其中Pid可以是一个表达式或常量,但其计算结果都必须为进程PID;Msg是发送给进程(Pid指向的进程)的消息,例如:
Pid ! {a,12}
将消息{a,12}发送给Pid(Process identifier的简写)指向的进程。在发送消息之前,会先计算“!”两边的表达式,因此
foo(12) ! math3:area({square,5})
会计算函数foo(12)(其返回值必须为有效的进程PID)和math3:area({square,5})然后将后者的结果(25)作为消息发送给前者指向的进程。然而它们的计算先后次序是不确定的。
receive用来接收消息,它的语法如下:
receive
Message1 ->
... ;
Message2 ->
... ;
...
end
这种形式表示尝试着接收一个消息,它可能和Message1、Message2、...中的一个匹配。进程计算receive时会先挂起(即什么也不做),当接收到一个消息而它又和Message1、Message2、...中的一个匹配时,就会计算相应“->”后面的代码。
当接收到一个消息时,任何出现在消息接收模式中未绑定的变量都会被绑定到消息中与之对应的值上。
receive的返回值是匹配成功的子句的值。
我们知道send(!)是发送消息,receive是接收消息。而更准确的说法是,send(!)将消息发送到目的进程的邮箱(mailbox)中,receive则是试着从当前进程的邮箱中删除消息。
receive是有选择的。它从等待进程处理的消息队列中取出第一个与消息模式匹配成功的消息。如果receive的消息模式没有一个匹配上,进程就会挂起,直到接收一个能匹配的消息,未匹配的消息会被保存留待以后处理。
1.5.1 一个echo进程
我们将创建一个echo进程作为并行处理的简单例子。这个进程显示出任何发给它的消息。我们假设进程A发送{A,Msg}给echo进程,于是echo进程发送给进程A一个包含Msg的新消息。示意图表示如下:
例如1.5中,echo:start()创建了一个简单echo进程,它返回发送给它的消息。
-module(echo).
-export([start/0,loop/0]).
start() ->
spawn(echo,loop,[]).
loop() ->
receive
{From,Message} ->
From ! Message,
loop()
end.
例程1.5
spawn(echo,loop,[])产生一个新进程并行的计算函数echo:loop()。于是
...
Id = echo:start(),
Id ! {self(),hello}
...
启动一个并行进程并向该进程发送消息{self(),hello}。其中self()是一个内建函数,它返回当前进程的PID。