这个世界是并行的。
如果希望将程序的行为设计得与真实世界物体的行为相一致,那么
程序就应该具有并发结构。
使用专门为并发应用设计的语言,开发将变得极为简便。
Erlang程序模拟了人类如何思考,如何交互。
——Joe Armstrong
1.=操作符(模式匹配)
当看到一个表达式像X = 123时,它的意思看似“将整数123赋予变量X”,但这种解读是不
正确的。=不是一个赋值操作符,它实际上是一个模式匹配操作符。与其他函数式编程语言一样,Erlang的变量只能绑定一次。绑定变量的意思是给变量一个值,
Erlang的变量是一次性赋值变量(single-assignment variable)。顾名思义,它们只能被赋值一次。如果试图在变量被设置后改变它的值,就会得到一个错误(事实上,你将得到的是我们刚
才看到的“badmatch”,即不匹配错误)。已被指派一个值的变量称为绑定变量,否则称为未绑定变量。一旦这个值被绑定,以后就不能改动了。
在Erlang里,变量获得值是一次成功模式匹配操作的结果。
在大多数语言里,=表示一个赋值语句。而在Erlang里,=是一次模式匹配操作。Lhs =Rhs
的真正意思是:计算右侧(Rhs)的值,然后将结果与左侧(Lhs)的模式相匹配。
变量(比如X)是模式的一种简单形式。如之前所说的,变量只能赋值一次。我们第一次说
X = SomeExpression时,Erlang对自己说:“我要做些什么才能让这条语句为真?”因为X还没
有值,它可以绑定X到SomeExpression这个值上,这条语句就成立了
2. 变量和原子的语法
请注意Erlang的变量以大写字母开头。所以X、This和A_long_name都是变量。以小写字母
开头的名称(比如monday或friday)不是变量,而是符号常量,它们被称为原子(atom)。
如果你在任何时候看到或写出像x = 123这样的表达式(注:这里的x是小写的,如果没有
注意到的话),那么几乎可以肯定这是一个错误
3. 元组
如果想把一些数量固定的项目归组成单一的实体,就会使用元组(tuple)。创建元组的方法
是用大括号把想要表示的值括起来,并用逗号分隔它们。举个例子,如果想要表示某人的名字和
图灵社区会员 宝马(328827135@qq.com) 专享 尊重版权
身高,就可以用{joe, 1.82}。这个元组包含了一个原子和一个浮点数。
Erlang没有类型声明,因此要创建一个“坐标点”,只需要这么写:
这样就创建了一个元组并把它绑定到变量P上。与C结构不同,元组里的字段没有名字。因
为这个元组只包含一对整数,所以必须记住它的用途是什么。为了更容易记住元组的用途,一种
常用的做法是将原子作为元组的第一个元素,用它来表示元组是什么。因此,我们会写成{point,
10,45}而不是{10, 45},这就使程序的可理解性大大增加了。这种给元组贴标签的方式不是语
言所要求的,而是一种推荐的编程风格。
4.列表
列表(list)被用来存放任意数量的事物。创建列表的方法是用中括号把列表元素括起来,
并用逗号分隔它们。
假设想要表示一个图形。如果假定此图形由三角形和正方形组成,就可以用一个列表来表
示它。这个图形列表里的每一个元素都是固定大小的元组(例如{square,Point, Side}或者
{triangle, Point1, Point2, Point3}),但这个图形本身可能包含任意数量的事物,因此用
列表来表示
5.模块与函数
geometry.erl
-module(geometry).
-export([area/1]).
文件的第一行是模块声明。声明里的模块名必须与存放该模块的主文件名相同。
第二行是导出声明。Name/N这种记法是指一个带有N个参数的函数Name,N被称为函数的元
数(arity)。export的参数是由Name/N项目组成的一个列表。因此,-export([area/1])的意思
是带有一个参数的函数area可以在此模块之外调用。
未从模块里导出的函数只能在模块内调用。已导出函数就相当于面向对象编程语言(OOPL)
里的公共方法,未导出函数则相当于OOPL里的私有方法。
area函数有两个子句。这些子句由一个分号隔开,最后的子句以句号加空白结束。每条子
句都有一个头部和一个主体,两者用箭头(->)分隔。头部包含一个函数名,后接零个或更多个
模式,主体则包含一列表达式,它们会在头部里的模式与调用参
数成功匹配时执行。这些子句会根据它们在函数定义里出现的顺序进行匹配。
6.逗号、分号、句号
逗号(,)分隔函数调用、数据构造和模式中的参数。
分号(;)分隔子句。我们能在很多地方看到子句,例如函数定义,以及case、if、
try..catch和receive表达式。
句号(.)(后接空白)分隔函数整体,以及shell里的表达式。
有一种简单的方法可以记住这些:想想英语。句号分隔句子,分号分隔子句,逗号则分隔下
级子句。逗号象征短程,分号象征中程,句号则象征长程。
请注意,最后的表达式(即关键字end之前的那条)没有分号。
7.fun:基本的抽象单元
Erlang是一种函数式编程语言。此外,函数式编程语言还表示函数可以被用作其他函数的参
数,也可以返回函数。操作其他函数的函数被称为高阶函数(higher-order function),而在Erlang
中用于代表函数的数据类型被称为fun。
高阶函数是函数式编程语言的精髓——函数式程序不仅可以操作常规的数据结构,还可以操
作改变数据的函数。一旦你学会如何使用它们,就会爱上它们。在后面会看到更多的高阶函数。
可以通过下列方式使用fun。
q 对列表里的每一个元素执行相同的操作。在这个案例里,将fun作为参数传递给
lists:map/2和lists:filter/2等函数。fun的这种用法是极其普遍的。
q 创建自己的控制抽象。这一技巧极其有用。例如,Erlang没有for循环,但我们可以轻松
创建自己的for循环。创建控制抽象的优点是可以让它们精确实现我们想要的做法,而不
是依赖一组预定义的控制抽象,因为它们的行为可能不完全是我们想要的。
q 实现可重入解析代码(reentrant parsing code)、解析组合器(parser combinator)或惰性求
值器(lazy evaluator)等事物。在这个案例里,我们编写返回fun的函数。这种技术很强
大,但可能会导致程序难以调试。
funs是“匿名的”函数。这样称呼它们是因为它们没有名字。你可能会看到其他编程语言
称它们为lambda抽象。
8.关卡
关卡(guard)是一种结构,可以用它来增加模式匹配的威力。通过使用关卡,可以对某个
模式里的变量执行简单的测试和比较。假设想要编写一个计算X和Y之间最大值的max(X, Y)函
数。可以像下面这样用关卡来编写它:
max(X,Y)when X>Y -> X;
max(X,Y)-> Y
子句1会在X大于Y时匹配,结果是X。
如果子句1不匹配,系统就会尝试子句2。子句2总是返回第二个参数Y。Y必然是大于或等于
X的,否则子句1就已经匹配了。
可以在函数定义里的头部使用关卡(通过关键字when引入),也可以在支持表达式的任何地
方使用它们。当它们被用作表达式时,执行的结果是true或false这两个原子中的一个。如果关
卡的值为true,我们会说执行成功,否则执行失败。
关卡序列(guard sequence)是指单一或一系列的关卡,用分号(;)分隔。对于关卡序列G1;
G2; ...; Gn,只要其中有一个关卡(G1、G2……)的值为true,它的值就为true。
关卡由一系列关卡表达式组成,用逗号(,)分隔。关卡GuardExpr1, GuardExpr2, ... ,
GuardExprN只有在所有的关卡表达式(GuardExpr1、GuardExpr2……)都为true时才为true。
9.Erlang 的类型表示法
预定义类型
指定函数的输入输出类型
函数规范说明了某个函数的参数属于何种类型,以及该函数的返回值属于何种类型。函数规
范的编写方式如下:
-spec functionName(T1,T2,…,Tn)->Tret when
Ti::Typei,
Tj::Typej,
…
这里的T1,T2,..., Tn描述了某个函数的参数类型,Tret描述了函数返回值的类型。如果
有必要,可以在可选关键字when后面引入额外的类型变量。
我们将从一个例子入手。下面这个类型规范:
-specfile:open(FileName,Modes)->{ok,Handle}|{error,Why} when
FileName::string(),
Modes::[Mode],
Mode::read|write|…
Handle::file_handle(),
Why::error_term.
说明如果打开FileName文件,得到的返回值不是{ok, Handle}就是{error, Why}。
FileName是一个字符串,Modes是一个由Mode组成的列表,而Mode是read、write等模式中的
参考gen_tcp.erl模块
-module(gen_tcp).
-export([connect/3,connect/4,listen/2,accept/1,accept/2,
shutdown/2,close/1]).
-export([send/2,recv/2,recv/3,unrecv/2]).
-export([controlling_process/2]).
-export([fdopen/2]).
-include("inet_int.hrl").
-include("file.hrl").
-typeconnect_option() ::
{ip,inet:socket_address()} |
{fd,Fd :: non_neg_integer()} |
{ifaddr,inet:socket_address()} |
inet:address_family() |
{port,inet:port_number()} |
{tcp_module,module()} |
option().
-typelisten_option() ::
{ip,inet:socket_address()} |
{fd,Fd :: non_neg_integer()} |
{ifaddr,inet:socket_address()} |
inet:address_family() |
{port,inet:port_number()} |
{backlog,B :: non_neg_integer()} |
{tcp_module,module()} |
option().
-typesocket() ::port().
-export_type([option/0, option_name/0, connect_option/0, listen_option/0,
socket/0]).
%%
%% Connect a socket
%%
-specconnect(Address,Port, Options) -> {ok, Socket} | {error, Reason} when
Address :: inet:socket_address() |inet:hostname(),
Port :: inet:port_number(),
Options :: [connect_option()],
Socket :: socket(),
Reason :: inet:posix().
connect(Address,Port, Opts) ->
connect(Address,Port,Opts,infinity).
-speclisten(Port,Options) -> {ok,ListenSocket} |{error,Reason} when
Port :: inet:port_number(),
Options :: [listen_option()],
ListenSocket ::socket(),
Reason :: system_limit |inet:posix().
listen(Port,Opts0) ->
{Mod,Opts} = inet:tcp_module(Opts0),
caseMod:getserv(Port)of
{ok,TP} ->
Mod:listen(TP,Opts);
{error,einval} ->
exit(badarg);
Other ->Other
end.