【erlang】语法篇

要讲清楚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中并没有布尔类型,而是使用原子的truefalse来表示布尔值。

原子

说实话我并不知道如何去解释原子这种类型,就像我不知道该如何解释数字类型。你写下一串数字,于是得到一个数字类型,我写下一串字符,于是得到一个原子类型。

原子类型的英文叫atom,表示不可分割的。比如abc是一个原子,它不同于字符串"abc"abc三个字符组成,原子类型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
    }).

在其他模块中使用pointcolor只需要写上-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

map是erlang R17才引入的用来表示映射的数据类型,语法类型记录。创建map的语法如下:

#{key1 => val1, key2 => val2, ... keyn => valn}.

erlang的map是有序的,按键的值升序排序。

更新map有两种方式,假设我们已经有了一个map:M1=#{a=>1, b=>2}

  1. M2=M1#{c=>3}
  2. 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:nn的取值是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.

函数表达式由funend界定,中间的内容和常规函数语法非常类似,不过依然有以下两点非常重要的区别:

  • 函数表达式的函数名是可选的。
  • 函数表示式的函数名是变量。这就意味着函数表达式中的函数名必须是大写字母开头,这一点一定要特别注意。

还是求斐波拉契数列的例子,这次我们在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的模块以文件为单位,而不是目录,一个文件就是一个模块。模块中可以定义各种属性,它们具有相似的语法:

-标签().

标签必须是原子,值必须是字面量。

模块属性

模块是存放函数的地方,模块属性是对模块的描述,放在模块开头。事实上我们已经见识过了moduleexport这两个属性,分别用来定义模块和导出函数。模块属性分为预定义属性和自定义属性,自定义属性可以随便写,不做过多介绍;下面主要介绍那些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/0attrs: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(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

以上都是运算符,以中缀的形式调用。

比较运算

运算符描述
<小于
>大于
=<小于等于
>=大于等于
==等于
/=不等于
=:=严格等于
=/=严格不等于

注意:

  • 小于等于和其他语言的写法是相反的。
  • 相比于等于和不等于,严格版本的等于和不等于还会判断类型,类型和值都相等才判定为相等。
  • erlang中任何类型之间都可以比较,规则如下:
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开始,andalsoorelse不再要求右边的表达式计算到布尔值。因此,它们现在是尾递归的。例如,以下函数在Erlang/OTP R13A和更高版本中是尾递归的。

all(Pred, [Hd|Tail]) ->
    Pred(Hd) andalso all(Pred, Tail);
all(_, []) ->
    true.

流程控制

if

语法:

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

语法:

case Expr of
    Pattern1 [when GuardSeq1] ->
        Body1;
    ...;
    PatternN [when GuardSeqN] ->
        BodyN
end

case的语法同函数有点像,如果所有的分支都匹配失败,则会引发一个运行时错误。

maybe

OTP 25新加的实验性功能,默认关闭,开启方式如下:

  • 第一步:在源代码中加入-feature(maybe_expr, enable).(紧挨在-module之后),获得语法上的通行证。
  • 第二步:使用特性编译代码,这里有以下两种方式可选:
    • 在erlang shell中编译:compile:file(first, {feature,maybe_expr,enable}).
    • 在命令行用erlc编译:erlc -enable-feature maybe_expr first.erl
  • 第三步:使用特性打开erlang shell,在命令行中执行erl -enable-feature maybe_expr,现在就可以在erlang shell中实验maybe表达式了。

maybe表达式的语法如下:

maybe
    Expr1,
    ...,
    ExprN
end

maybeend之间的表达式会依次被求值,最后一个表达式的值也是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/0b/0返回error,根据?=的规则,maybe表达式返回error,后面的表达式都不会求值。如果a/0返回负数,第二个=模式匹配失败,异常退出。

maybe表达式中还可以带一个else表达式,语法如下:

maybe
    Expr1,
    ...,
    ExprN
else
    Pattern1 [when GuardSeq1] ->
        Body1;
    ...;
    PatternN [when GuardSeqN] ->
        BodyN
end

?=匹配失败是,它的值会和else中的模式进行匹配,如果匹配上,对应分支的表达式会做为整个maybe表达式的结果,如果没有分支匹配上,则会引发一个运行时异常。

注意maybeelse是两个独立分作用域,maybe中绑定的变量是不能在else中使用的。

示例:

maybe
    {ok, A} ?= a(),
    true = A >= 0,
    {ok, B} ?= b(),
    A + B
else
    error -> error;
    wrong -> error
end
catch

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

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是异常的类,有throwexiterrorStacktrace必须是一个变量,用来接收调用栈,这两都是可省略的。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 <- ListExprListExpr是一个可以生成列表的表达式。
  • 二进制生成器:BitstringPattern <= BitStringExprBitStringExpr是一个可以产生二进制流的表达式。

列表生成器既可以用于列表推导,也可以用于二进制推导,二进制生成器也一样。

过滤器是一个产生布尔值的表达式,如果返回值不是truefalse,会产生一个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>>

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值