要讲清楚erlang的语法,这一章的压力是巨大的,有些遗漏的地方我们会在后面的专题中补充。
erlang只有两种数字类型:整数和浮点数。
整数宽度仅受限于内存大小,erlang整数有两种书写格式:
base#value
:base进制的整数,base的范围是2至36,十进制可省略base#
,value可以使用下划线分隔。$char
:字符char的ASCII码,支持转义字符,比如$\n
。浮点数都是64位,遵守IEEE 754标准。浮点数语法没啥特别,可以添加下划线,支持科学计数法,0.1+0.2≠0.3
,都是与你的经验相符的。唯一需要注意的是小于0的浮点数中整数部分的0是不能省略的,某些语言可以写.1
表示0.1,erlang不支持。
关于0.1+0.2=0.30000000000000004
这件事情,居然还有一个专门的网站:https://0.30000000000000004.com/,推荐浏览一下。
erlang的字符串使用""
包裹。注意erlang并没有字符串类型,String只是整数列表的一种简写形式,许多函数式语言都是如此。比如"abc"
就等价于[$a,$b,$c]
。字符串中可以使用\x{}
包含Unicode字符,比如"a\x{221e}b"
表示"a∞b"
。
erlang shell在打印整数列表时,如果发现每一项都是可打印的ASCII字符,就会以字符串的形式打印列表。如果要按列表打印,可以使用io:format("~w~n~, ["abc"]).
,如果想打印出列表中的Unicode字符,比如中文,就需要使用io:format("~ts~n", ["你好"]).
。
erlang中比较神奇的一点是相邻的两个字符串会被合并成一个。
1> "ab" "cd".
"abcd"
erlang中并没有布尔类型,而是使用原子的true
和false
来表示布尔值。
说实话我并不知道如何去解释原子这种类型,就像我不知道该如何解释数字类型。你写下一串数字,于是得到一个数字类型,我写下一串字符,于是得到一个原子类型。
原子类型的英文叫atom,表示不可分割的。比如abc
是一个原子,它不同于字符串"abc"
由a
,b
,c
三个字符组成,原子类型abc
就是一个整体,不能分开来看,就像数字123
就是一个整体。
原子不需要定义,这大概是最令人困惑的地方。千万不要把原子理解为变量或常量,更不是字符串。它更接近于数字,不管你是否申明或者定义,所有的数字天然就是存在的,原子也是如此,理解了数字,就理解了原子。
在erlang中,原子类型包裹在''
中,可以是任意的ASCII字符,如果是小写字母开头,可以省略单引号,下面是一些原子类型示例:
hello
hello_world
hello@world
hello12345
'hello'
'hello world'
'+'
'123'
'@#$%'
熟悉lisp的人一定不会对原子类型感到陌生,但对于c家族语言程序员来说,原子类型着实令人费解。racket中有一种叫keyword的类型,比如
'abc
,如果你能理解它,那么原子与之是类似的。
列表是函数式语言的基石,如果你学习过Haskell语言,那么应该能瞬间理解erlang的列表。
列表是递归定义的,描述为一个头部“加”一个尾部。尾部可以是一个列表或空列表,空列表是不包含任何元素的特殊列表,是递归退出条件。这里所说的“加”在代码层面面表示一个连接符,erlang用的是|
,Haskell用的是:
。
erlang使用[]
表示空列表,非空列表表示为[H|T]
,其中T
是一个列表。erlang列表中的元素可以是任意类型,不要求同类型。下面是一些例子。
[] %空列表
[a|[]] %[a]
[a|[b|[]]] %[a,b]
[a|[b|[c|[]]]] %[a,b,c]
递归嵌套是列表的本质,不过写起来太费劲,erlang提供了简写语法,使用逗号分隔元素,如[1,2,a,b]
。
元组是一种长度固定的复合数据类型,使用{}
表示,元素之间使用,
分隔。在erlang中,无论是元组本身还是它的字段都是匿名的。作为一种编程风格,erlang习惯将元组的名称作为元组的第一个元素。比如一个表示二维坐标的元组可以写成{point, 1 ,3}
,元组是可以嵌套的,或者我们可以更近一步写成{point, {x, 1}, {y, 2}}
。注意这只是一种编程风格,与语法无关。
记录语法可以创建具名元组,就像结构体一样,记录不是一种新的数据类型,本质上它还是元组,编译时会完成这一转换。语法如下:
-record(元组名, {
字段1 [= 默认值1],
字段2 [= 默认值2],
... ...
字段n [= 默认值n]
}).
记录语法只能用于源文件,无法直接在shell中使用,因为它不是表达式,要在shell中使用需要一些特殊的骚操作。所有的=默认值
都是可以省略的,上面的[]
表示可省略的,而不是语法。如果在创建元组时未给字段赋值,就会使用默认值,若没有默认值则未undefined
。
在.erl
文件中定义的记录无法导出,要在模块外使用只能再导出一个“构造函数”来创建记录。
%first.erl
-module(first).
-export([newPoint/2]).
-record(point, {x, y}). %定义记录
%注意参数X和Y是变量,要大写
newPoint(X,Y) ->
#point{x=X, y=Y}.
然后就可以在erlang shell中生成point
元组了。
1> c(first).
{ok,first}
2> first:newPoint(1,2).
{point,1,2}
以上方式只适合模块内使用,如果需要多模块共享,可以将记录写到.hrl
文件中,它类似于头文件,可以被其他文件包含,并且.hrl
文件中的记录定义不需要导出。
新建records.hrl
,输入以下内容。
%records.hrl
-record(point, {x, y}).
-record(color, {
r = 0,
g = 0,
b = 0
}).
在其他模块中使用point
或color
只需要写上-include("records.hrl")
即可,可以写相对路径,也可以写绝对路径。
在erlang shell中使用可以通过rr("records.hrl")
读入头文件。
1> rr("records.hrl").
[color,point]
2> #point{x=1,y=2}.
#point{x = 1,y = 2}
3> #color{r=255}.
#color{r = 255,g = 0,b = 0}
map是erlang R17才引入的用来表示映射的数据类型,语法类型记录。创建map的语法如下:
#{key1 => val1, key2 => val2, ... keyn => valn}.
erlang的map是有序的,按键的值升序排序。
更新map有两种方式,假设我们已经有了一个map:M1=#{a=>1, b=>2}
。
M2=M1#{c=>3}
M2=M1#{b:=3}
这两种方式可以混用,比如M2=M1#{c=>3, b:=2}
,无论哪种方式都会得到一个新的map,新map在旧map上复制而来,但这种复制是轻量的,旧map并不受影响。
这两种更新方式的一个重要区别是当要更新的键不存在时,=>
会将键值插入map,而:=
会得到一个错误。使用:=
更新map有两个好处:
:=
不会插入新的键,因此新旧map可以共用键描述符,当map非常大时,可以节省不少空间。建议是除非要插入新的键,否则应该使用:=
更新map。
二进制和比特流用来表示一块无类型的内存区域,在其他语言中有字节数组或字符指针表达类似的含义。二进制和比特流的区别是当一个比特流中的比特数是8的整数倍时,它就是二进制。
创建二进制或比特流的语法称为位语法,描述如下:
<<>>
<<E1, E2, ... Ei, ... En>>
其中每个Ei
具有以下形式之一:
Value
Value:位宽
Value/End-Sign-Type-Unit
Value:位宽/End-Sign-Type-Unit
位宽表示Value
占用几个比特,Value
超出位宽会被截断,默认8比特。
End
表示字节顺序(大端序or小端序),有3个可选值:
big
:大端序,默认值。little
:小端序。native
:由机器的CPU决定。Sign
表示整数有无符号,仅用于模式匹配,有两个可选值:
signed
:有符号整数。unsigned
:无符号整数,默认值。Type
表示元素类型,可选值如下:
integer
:默认值。float
binary
bytes
bitstring
bits
utf8
utf16
utf32
Unit
的语法为unit:n
,n
的取值是1至256。它必须和位宽一起使用,一个元素的总长度等于位宽 × unit
。
示例1,创建二进制,可以使用整数或者字符串。
1> X = <<1,2>>.
<<1,2>>
2> Y = <<"abc">>.
<<"abc">>
有一点需要注意的是
=
和<<
之间,以及>>
和=
之间一定要用空格隔开,否则erlang会将=
和<
解释为<=
,将>
和=
识别为>=
,造成语法错误。对于二进制的打印方式,也遵循列表的打印规则。
示例2,指定位宽。
3> Z = <<1:2, 4:3>>.
<<12:5>>
这是一个比特流,共5个比特,前两个比特是01
,后3个比特是100
,所以最终结果是01100
,十进制就是12。
示例3,大小端。
4> M = <<573:16/big>>.
<<2,61>>
5> N = <<573:16/little>>.
<<61,2>>
示例4,浮点数。
6> K = <<1.0/float>>.
<<63,240,0,0,0,0,0,0>>
位语法非常适合做协议开发,并且erlang针对二进制做了优化,效率非常高。相比于其他语言去移位,按位与或,erlang位语法结合模式匹配简直爽歪歪。
更绝的是位语法的模式匹配中,后面的项可以使用前面的结果,比如:
<<Size:4, Data:Size/binary, ...>>
第一项我们将前4比特解码到Size
,接着又使用这个Size
提取Data
。
此外,erlang提供了两个有用的函数,可以完成任意的erlang类型和二进制之间的转换,因此可以方便的将erlang类型写入磁盘或网络。
term_to_binary
:将erlang类型转成二进制。binary_to_term
:将二进制转成erlang类型。3> B = term_to_binary({color,1,2,3}).
<<131,104,4,100,0,5,99,111,108,111,114,97,1,97,2,97,3>>
4> binary_to_term(B).
{color,1,2,3}
做为函数式编程语言,函数也是一种类型。标定一个函数的三元组是模块名:函数名/元数
,元数就是函数参数的个数。
我们在.erl
文件中书写的函数都是常规函数,语法如下:
函数名(模式11,...,模式1n) [when 关卡序列1] ->
函数体1;
...;
函数名(模式k1,...,模式kn) [when 关卡序列k] ->
函数体k.
每个->
前面的叫做函数头,后面的叫做函数体,函数体由若干表达式构成,表达式之间通过,
分隔。每个函数头->函数体;
称为一个函数子句,一个完整的函数可以由若干函数子句构成,子句之间使用;
分隔,最后一个子句以.
结尾。你可以仔细体会以下elang对于标点符号的使用,是不是挺符合经验直觉的。
函数名是一个原子,因此一般是小写字母开头,如果想用大写字母开头,需要用''
包裹,调用时也要这样写,这是原子的规则。
调用函数时,每个参数会和对应的模式去匹配,因此同一个函数的每个子句的模式数量必须相同,即拥有相同的元数。一旦参数和模式匹配上,就会执行这个子句。当然,还要后面的关卡测试也通过。
when
关键字后面的是关卡序列,是可选的。关卡序列由若干布尔表达式组成,它们之间可以使用;
或,
分隔,区别是;
表示逻辑或,而,
表示逻辑与,这一点也与经验直觉相符。
关卡测试是对模式匹配的补充,函数本质上就是一系列表达式的集合,通过参数来决定执行哪些表达式,而这个决定的过程就是模式匹配和关卡测试。
下面是一个典中典示例,求斐波拉契数列。
fact(N) when N>0 ->
N * fact(N-1);
fact(0) ->
1.
函数表达式常用于高阶函数和erlang shell,将一个函数作为另一个函数的返回值时,也会用到函数表达式。函数表达式语法如下:
fun
[函数名](模式11,...,模式1n) [when 关卡序列1] ->
函数体1;
...;
[函数名](模式k1,...,模式kn) [when 关卡序列k] ->
函数体k
end.
函数表达式由fun
和end
界定,中间的内容和常规函数语法非常类似,不过依然有以下两点非常重要的区别:
还是求斐波拉契数列的例子,这次我们在erlang shell中实现。
1> Fun1 = fun Fact(1) -> 1; Fact(X) when X > 1 -> X * Fact(X - 1) end.
#Fun<erl_eval.19.3316493>
2> Fun1(30).
265252859812191058636308480000000
如果要将一个常规函数变成一个函数表达式,我们可以通过直接在表达式中调用函数来实现。
fun (Arg1,...,ArgN) -> Name(Arg1,...,ArgN) end.
但是这样写比较冗长,于是erlang提供了下面的语法糖。
fun Name/Arity.
fun Module:Name/Arity.
连end
都省了,可以说是非常贴心了。
3> Fun2 = fun first:fact/1.
fun first:fact/1
4> Fun2(30).
265252859812191058636308480000000
这里的引用并不是C++里的引用,erlang的引用是一种全局(包括分布式erlang集群)唯一的数据类型。用途是创建一个独一无二的标签。引用通过内置函数(BIF,Buid In Func)make_ref/0
创建,内置函数is_reference/1
用来判断一个变量是不是引用。
1> R1 = make_ref().
#Ref<0.2310775775.850132995.77305>
2> is_reference(R1).
true
erlang的模块以文件为单位,而不是目录,一个文件就是一个模块。模块中可以定义各种属性,它们具有相似的语法:
-标签(值).
标签必须是原子,值必须是字面量。
模块是存放函数的地方,模块属性是对模块的描述,放在模块开头。事实上我们已经见识过了module
和export
这两个属性,分别用来定义模块和导出函数。模块属性分为预定义属性和自定义属性,自定义属性可以随便写,不做过多介绍;下面主要介绍那些erlang预定义的属性。
-module(模块名).
定义一个模块的名字,必须是文件的第一行代码,且不可省略。模块名是一个原子,必须和文件名(不带后缀)相同,否则erlang的代码加载机制无法工作。
-export([函数名/元数, ... 函数名/元数]).
导出函数,可以有0到多个,只有导出的函数才能在模块外访问。
-import(模块名, [函数名/元数, ... 函数名/元数]).
导入其他模块的函数,一般我们通过模块名:函数名
的方式调用函数,而导入之后,就可以直接通过函数名
来调用了。
-compile(options).
编译选项,如果有多个,需要放到元组或者列表里,如{option1, ... optionN}
或者[option1, ... optionN]
。在开发阶段一个比较有用的选项是export_all
,它会导出所有函数,此时编译器会给你一个警告,可以使用nowarn_export_all
消除这个警告。完整的编译选项可以查看官方文档。
-vsn(版本号)
版本号可以是一个值或者一个列表,如果有多个-vsn
那么所有版本号最终也会被拼接成一个列表。可以通过beam_lib:version/1
函数来查看一个模块的版本号,参数是模块名,它输出的是{ok, {模块名, [版本号]}}
。如果不指定,默认以模块的MD5值作为版本号。
-on_load(函数名/0).
在模块被加载后自动执行一个函数。这个函数必须是无参函数且必须返回ok
,可以是非导出的,它会在一个新的进程里执行,函数执行完以后,进程立刻终止。
-nifs([函数/元数, ... 函数/元数])
指明那些函数从动态链接库(.dll
,.so
)里加载。NIF
的含义是Native Implemented Functions。
提取模块属性有两种方式:
beam_lib:chunks/2
:可以在不载入代码的情况下提取属性。模块名:module_info/0
和attrs:module_info/1
:需要先加载代码,这两个函数是由编译器内置到模块中的。示例:
1> beam_lib:chunks("first.beam", [attributes]).
{ok,{first,[{attributes,[{vsn,[338263298016390874136079198029794793513]}]}]}}
2> beam_lib:chunks("first.beam", [exports]).
{ok,{first,[{exports,[{'NewPoint',2},
{fact,1},
{maxx,2},
{module_info,0},
{module_info,1},
{start,0}]}]}}
3> first:module_info().
hello erlang
[{module,first},
{exports,[{start,0},
{'NewPoint',2},
{fact,1},
{maxx,2},
{module_info,0},
{module_info,1}]},
{attributes,[{vsn,[338263298016390874136079198029794793513]}]},
{compile,[{version,"8.2.3"},
{options,[]},
{source,"A:/.../.../.../first.erl"}]},
{md5,<<254,123,36,55,158,189,141,243,105,179,102,17,130,
52,88,41>>}]
3> first:module_info(compile).
[{version,"8.2.3"},
{options,[]},
{source,"A:/.../.../.../first.erl"}]
预处理属性会在编译之前处理。
-include("file.hrl").
用来导入record文件。
-define(Macro, Replacement).
用来定义宏。
-record(Record,Fields).
用来定义具名元组,我们已经在记录语法章节中见识过它了。
函数属性是用来描述函数的属性,用来补充函数的信息。
-file(filename, line).
filename
必须是字符串,line
必须是正整数。erlang有两个预定义宏?FILE
和?LINE
表示一个函数所在的文件名和行号,当发生异常时,erlang会告诉你异常的函数在哪个文件的第几行。这个属性可以修改?FILE
和?LINE
宏,当某个函数的实现和声明不在一起的时候,这个属性就派上用场了,它可以告诉开发者异常函数的真正实现在哪里。
在first.erl
中输入以下代码:
-module(first).
-file("我在这里", 573).
double(I) -> 2 * I.
编译上面的代码会看到如下输出:
1> c(first).
我在这里:574:1: Warning: function double/1 is unused
{ok,first}
由于我们并没有导出`double/1`函数,所以erlang给了我么一条警告,看开头那里,文件名和行号已经是我们指定的值了。注意,这个属性会影响它之后的所有函数,而不是紧挨着它的那一个函数。
-spec my_function(integer()) -> integer().
这个属性是对函数类型的补充说明,指示函数出入参的类型,当你在vscode中将鼠标放到io:format
函数上是,就会看到一个提示框,里面是一些-spec
告诉你该函数入参是什么的时候返回值是什么,类似于一种文档。
-type my_type() :: atom() | integer().
与-spec
类似,不过它是用来描述类型的。
-feature(FeatureName, enable | disable)
用来开启erlang特性,必须放在模块开头,-moudle
之后,-export
之前。
函数式语言里没有语句的概念,一切皆是表达式。
运算符 | 描述 | 参数类型 |
---|---|---|
+ | 正数 | Number |
- | 负数 | Number |
+ | 加 | number |
- | 减 | Number |
* | 乘 | Number |
/ | 浮点除法 | Number |
bnot | 按位取反 | Integer |
div | 整数除法 | Integer |
rem | 取余 | Integer |
band | 按位与 | Integer |
bor | 按位或 | Integer |
bxor | 按位异或 | Integer |
bsl | 算术左移 | Integer |
bsr | 算术右移 | Integer |
以上都是运算符,以中缀的形式调用。
运算符 | 描述 |
---|---|
< | 小于 |
> | 大于 |
=< | 小于等于 |
>= | 大于等于 |
== | 等于 |
/= | 不等于 |
=:= | 严格等于 |
=/= | 严格不等于 |
注意:
number < atom < reference < fun < port < pid < tuple < map < nil < list < bit string
示例:
1> 1 == 1.0.
true
2> 1 =:= 1.0.
false
3> 1 /= 1.0.
false
4> 1 =/= 1.0.
true
5> 1 < a.
true
运算符 | 描述 |
---|---|
not | 逻辑非 |
and | 逻辑与,两边都会求值 |
or | 逻辑或,两边都会求值 |
xor | 逻辑异或,两边都会求值 |
andalso | 短路逻辑与 |
orelse | 短路逻辑或 |
从Erlang/OTP R13A开始,andalso
和orelse
不再要求右边的表达式计算到布尔值。因此,它们现在是尾递归的。例如,以下函数在Erlang/OTP R13A和更高版本中是尾递归的。
all(Pred, [Hd|Tail]) ->
Pred(Hd) andalso all(Pred, Tail);
all(_, []) ->
true.
语法:
if
GuardSeq1 ->
Body1;
...;
GuardSeqN ->
BodyN
end
一般会在最后加一个true
分支作为else,没有true
分支并不会导致编译错误,但是在运行时,一旦所有分支都匹配失败,就会引发异常。
示例:
is_greater_than(X, Y) ->
if
X>Y ->
true;
true -> % works as an 'else' branch
false
end.
语法:
case Expr of
Pattern1 [when GuardSeq1] ->
Body1;
...;
PatternN [when GuardSeqN] ->
BodyN
end
case的语法同函数有点像,如果所有的分支都匹配失败,则会引发一个运行时错误。
OTP 25新加的实验性功能,默认关闭,开启方式如下:
-feature(maybe_expr, enable).
(紧挨在-module
之后),获得语法上的通行证。compile:file(first, {feature,maybe_expr,enable}).
erlc
编译:erlc -enable-feature maybe_expr first.erl
erl -enable-feature maybe_expr
,现在就可以在erlang shell中实验maybe表达式了。maybe
表达式的语法如下:
maybe
Expr1,
...,
ExprN
end
maybe
和end
之间的表达式会依次被求值,最后一个表达式的值也是maybe
表达式的值。
maybe
表达式中支持一种称为条件匹配的模式匹配,语法是Expr1 ?= Expr2
。如果匹配成功,它和正常的模式匹配Expr1 = Expr2
没啥区别,如果它是maybe
中的最后一个表达式,则Expr2
也是maybe
表达式的值;如果匹配失败,它会将后面的表达式短路,并返回Expr2
做为maybe
的值。
示例:
maybe
{ok, A} ?= a(),
true = A >= 0,
{ok, B} ?= b(),
A + B
end
正常情况下,如果函数a/0
返回{ok,1}
,b/0
返回{ok,2}
,那么maybe
表达式返回1+2
等于3
。如果a/0
或b/0
返回error
,根据?=
的规则,maybe
表达式返回error
,后面的表达式都不会求值。如果a/0
返回负数,第二个=
模式匹配失败,异常退出。
maybe
表达式中还可以带一个else
表达式,语法如下:
maybe
Expr1,
...,
ExprN
else
Pattern1 [when GuardSeq1] ->
Body1;
...;
PatternN [when GuardSeqN] ->
BodyN
end
当?=
匹配失败是,它的值会和else
中的模式进行匹配,如果匹配上,对应分支的表达式会做为整个maybe
表达式的结果,如果没有分支匹配上,则会引发一个运行时异常。
注意maybe
和else
是两个独立分作用域,maybe
中绑定的变量是不能在else
中使用的。
示例:
maybe
{ok, A} ?= a(),
true = A >= 0,
{ok, B} ?= b(),
A + B
else
error -> error;
wrong -> error
end
catch
用来捕获异常,语法如下:
catch Expr
返回Expr
的值,如果发生异常,返回异常值。示例如下:
1> catch 1+2.
3
2> catch 1+a.
{'EXIT',{badarith,[...]}}
erlang有三种方式显示生成一个错误:
throw(Term)
返回Term
。exit(Term)
返回{'EXIT',Term}
。error(Term)
返回{'EXIT',{Reason,Stack}}
,Reason
是异常原因,Stack
是调用栈。相比于其他语言要做大量防御式编程,erlang的防御是内建的,因此erlang函数往往可以只处理正常数据,对于异常数据则任其崩溃,监控进程表示:没关系,我会出手。
try
表达式是catch
的升级版,完整语法如下:
try Exprs of
Pattern1 [when GuardSeq1] ->
Body1;
...;
PatternN [when GuardSeqN] ->
BodyN
catch
Class1:ExceptionPattern1[:Stacktrace] [when ExceptionGuardSeq1] ->
ExceptionBody1;
...;
ClassN:ExceptionPatternN[:Stacktrace] [when ExceptionGuardSeqN] ->
ExceptionBodyN
after
AfterBody
end
嗯…try-catch-finally…妥了。try
代码块和case
非常像,不再赘述。catch
块用来处理异常,不要和catch
表达式混淆。Classi
是异常的类,有throw
、exit
和error
;Stacktrace
必须是一个变量,用来接收调用栈,这两都是可省略的。ExceptionPatterni
是用来匹配异常值的模式,ExceptionGuardSeqi
是关卡,这些我们已经很熟悉了。如果catch
块没能和异常匹配,异常就会继续往外抛。after
块用来在最后执行一些清理工作,无论有无异常或异常有无被捕获,它都会执行,而且它的值会被丢弃,但如果AfterBody
发生异常,它是不会被捕获的,而且会覆盖之前的异常。
完整的try
表达式语法比较复杂,它有几个简化版本。
首先是after
块可以被省略,如下:
try Exprs of
Pattern1 [when GuardSeq1] ->
Body1;
...;
PatternN [when GuardSeqN] ->
BodyN
catch
Class1:ExceptionPattern1[:Stacktrace] [when ExceptionGuardSeq1] ->
ExceptionBody1;
...;
ClassN:ExceptionPatternN[:Stacktrace] [when ExceptionGuardSeqN] ->
ExceptionBodyN
end
其次是try
代码块可以简化,即去掉of
及其后面的模式分支,如下:
try Exprs
catch
Class1:ExceptionPattern1[:Stacktrace] [when ExceptionGuardSeq1] ->
ExceptionBody1;
ClassN:ExceptionPatternN[:Stacktrace] [when ExceptionGuardSeqN] ->
ExceptionBodyN
end
最后catch
块也可以简化,省略掉class
,如下:
try Exprs
catch
ExceptionPattern1 [when ExceptionGuardSeq1] ->
ExceptionBody1;
ExceptionPatternN [when ExceptionGuardSeqN] ->
ExceptionBodyN
end
示例1,读取文件:
termize_file(Name) ->
{ok,F} = file:open(Name, [read,binary]),
try
{ok,Bin} = file:read(F, 1024*1024),
binary_to_term(Bin)
after
file:close(F)
end.
示例2,匹配class
:
try Expr
catch
throw:Term -> Term;
exit:Reason -> {'EXIT',Reason}
error:Reason:Stk -> {'EXIT',{Reason,Stk}}
end
表达式序列可以通过begin...end
打包成一个表达式,如果如下:
begin
Expr1,
...,
ExprN
end
块表达式的值是最后一个表达式ExprN
的值。
erlang中的=
并不是赋值,而是模式匹配,语法如下:
Expr1 = Expr2
模式匹配匹配的是构造函数,所谓模式就是一个值的构造形式,或者说是它具有的形状。模式匹配是函数式语言提取数据结构的唯一方式,学习函数式语言必须掌握模式匹配。
erlang模式匹配要求=
右边的表达式不能有未绑定变量,但左边的表达式可以有未绑定变量。如果模式匹配成功,那么左边未绑定的变量都会被绑定,右边表达式的值会做为整个模式匹配的结果返回。
例子:
1> 1=1.
1
2> 1=2.
** exception error: no match of right hand side value 2
3> {A,B}={1,a}.
{1,a}
4> A.
1
5> B.
a
6> {1,C}={1,2}.
{1,2}
7> C.
2
8> [H|T]=[1,2,3].
[1,2,3]
9> H.
1
10> T.
[2,3]
11> [1|L]=[1,2,3].
[1,2,3]
12> L.
[2,3]
1=1
居然也是模式匹配大概是许多初学者最不能理解的地方,我们可以做一个类比来帮助你理解。
数字是一种类型,就像三角形是一种形状。你写下1和2就像随手画两个三角形,它们肯定不会是完全一样的。而浮点数就像是四边形,不管你怎么画,肯定不会和三角形一样。而像列表、元组这样的类型就像是可以容纳其他图元的形状。总之,不同类型会有不同形状,同一种类型的不同值之间形状也不会完全一样。所谓模式,就是值所具备的形状,而模式匹配,就是就是在匹配这些不同的形状。
对于变量而言,在模式匹配之前,它们处于任何形状的叠加态,一旦模式匹配完成,它们就会坍缩到一种具体的形状,属实是薛定谔的变量了。
++
和--
erlang中的++
和--
不是自增和自减,甚至都和整数没有关系,它们是用来拼接和删除列表的,语法如下:
Expr1 ++ Expr2
Expr1 -- Expr2
示例:
1> [1,2,3]++[4,5].
[1,2,3,4,5]
2> [1,2,3,2,1,2]--[2,1,2].
[3,1,2]
注意,对于--
来说,右边列表中的每个元素在左边列表里只会删除一次。
列表推导和二进制推导的语法如下:
[Expr || Qualifier1,...,QualifierN]
<< <<Expr>> || Qualifier1,...,QualifierN >>
Qualifieri
可以是一个生成器或一个过滤器。生成器有以下两种:
Pattern <- ListExpr
,ListExpr
是一个可以生成列表的表达式。BitstringPattern <= BitStringExpr
,BitStringExpr
是一个可以产生二进制流的表达式。列表生成器既可以用于列表推导,也可以用于二进制推导,二进制生成器也一样。
过滤器是一个产生布尔值的表达式,如果返回值不是true
或false
,会产生一个bad filter的运行时异常。
对于二进制推导,还有一点需要格外注意,如果Expr
是包含运算符的复杂表达式,必须用括号()
包裹。
示例:
1> [X*Y || X<-[1,2,3],Y<-[1,2,3]].
[1,2,3,2,4,6,3,6,9]
2> [X*Y || X<-[1,2,3],Y<-[1,2,3],X*Y>3].
[4,6,6,9]
3> [X*Y || X<-[1,2,3],Y<-[1,2,3], X>1 andalso Y>2].
[6,9]
4> [X || <<X>> <= <<1,2>>].
[1,2]
5> << <<X>> || X <- [1,2,3]>>.
<<1,2,3>>
6> << <<(X+Y)>> || <<X>> <= <<1,2>>, Y <- [3,4] >>.
<<4,5,5,6>>