基本概念
本章介绍 Erlang 编程的背景。所有 Erlang 程序,无论并行还是顺序,都用到模式匹配、单赋值变量,以及 Erlang 用来表示数据的那些基本类型。
在本章中,我们将使用 Erlang shell 对系统进行实验,看看他会如何行事。我们将从 shell 开始。
启动与停止 Erlang shell
在 Unix 系统(包括 Mac OS)上,咱们在命令提示符处,启动 Erlang shell;在 Windows 系统上,则要点击 Windows 开始菜单中的 Erlang 图标(译注:在 CMD/PowerShell,或 Git Bash/MinGW/MSYS2 终端窗口中,也能获得 *nix 的体验)。
$ erl
Erlang/OTP 28 [erts-16.0.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
Eshell V16.0.2 (press Ctrl+G to abort, type help(). for help)
这是启动 Erlang shell 的 Unix 命令。shell 以一个告诉咱们正在运行哪个版本 Erlang 的横幅响应。停止系统的最简单方法,是按下 Ctrl+C
(Windows 上 Ctrl+Break
),然后按 a
(abort 的缩写),如下所示:
BREAK: (a)bort (A)bort with dump (c)ontinue (p)roc info (i)nfo
(l)oaded (v)ersion (k)ill (D)b-tables (d)istribution
a
$
输入 a
会立即停止系统,并可能导致一些数据损坏。要有控制地关闭系统,咱们可键入 q()
(quit 的简写)。
$ erl
Erlang/OTP 28 [erts-16.0.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
Eshell V16.0.2 (press Ctrl+G to abort, type help(). for help)
1> q().
ok
2> % $ werl
Erlang/OTP 28 [erts-16.0.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
Eshell V16.0.2 (press Ctrl+G to abort, type help(). for help)
1> q().
ok
2> %
译注:这是在 Windows 系统上,使用
erl
与werl
启动 Erlang shell 后,分别输入q().
后的行为。
这会以受控方式停止系统。所有打开的文件都会刷新并关闭,打开的数据库会被关闭(若在运行),且所有应用都会以有序方式关闭。q()
是 init:stop()
命令的 shell 别名。
要立即停止系统,请执行表达式 erlang:halt()
。
若这些方法都不起作用,请阅读 停止 Erlang。
在 shell 中执行命令
当 shell 准备好接受某个表达式时,他会打印出命令提示符。
1> X = 20.
20
咱们将看到,对话是从命令 1 处开始的(即 shell 打印了 1>
)。这意味着我们已启动了个新的 Erlang shell。在本书中,每当咱们看到一个以 1>
开始的对话时,如果咱们想要 完全 重现书中的示例,就必须启动一个新的 shell。当某个示例以大于 1 的提示数字开始时,这意味着该 shell 会话,是延续之前的示例,因此咱们不必启动一个新 shell。
提示符处我们输入一个表达式。shell 就会计算该表达式并打印结果。
2> X + 20. % and this is a comment
40
shell 打印出了另一个提示符,这次是表达式 2 的(因为每次输入一个新的表达式,命令编号都会增加)。
在第 2 行中,百分号 (%
) 表示一条注释的开始。从百分号到行尾的所有文本,都被视为一条注释,会被 shell 和 Erlang 编译器忽略。
现在也许是对 shell 进行实验的好时机。请输入示例中那些与课文中完全相同的表达式,并检查咱们是否会得到与书中同样的结果。有些命令序列可以输入多次,但有些命令序列则只能输入一次,因为他们依赖于之前的命令。在出现任何错误时,最好的办法是中止该 shell,然后用一个新启动的 shell 再试一次。
可能出错的事情
咱们不能把在本书中读到的所有内容,都输入 shell。Erlang 模组中的语法形式,就并非表达式,进而 shell 无法理解。尤其是,咱们无法在 shell 中输入注解;注解是以连字符开头的(如 -module
、-export
等)。
可能出错的情况是,咱们开始键入了带引号的内容(即以单引号或双引号开头),但尚未键入与开头引号相同的结尾引号。
如果出现上述情况,最好键入一个额外的引号,然后用点空白完成该命令。
高级:咱们可启动和停止多个外壳。详情请参阅 shell 不再响应。
Erlang shell 中命令的编辑
Erlang shell 包含了个内置的行编辑器。他能理解流行的 Emacs 编辑器中,所使用行编辑命令的子集。只需敲几下键盘,就能调回并编辑前几行。可用的命令如下所示(注意,^Key
表示咱们应按下 Ctrl+Key
):
命令 | 描述 |
---|---|
^A | 行开头处 |
^D | 删除当前字符 |
^E | 行结束处 |
^F 或右箭头 | 向前一个字符 |
^B 或左箭头 | 向后一个字符 |
^P 或向上箭头 | 前一行 |
^N 或向下箭头 | 下一行 |
^T | 专治最后两个字符 |
Tab | 尝试扩展当前模组或函数的名字 |
随着经验的积累,咱们将发现 shell 是个非常强大的工具。最重要的是,当咱们开始编写分布式程序时,咱们将发现咱们可以把一个 shell, 连接到集群中不同 Erlang 节点上运行的 Erlang 系统,甚至可以直接通过安全 shell (ssh
),连接到运行在远端计算机上的 Erlang 系统。这样,咱们就可以与 Erlang 节点系统中,任何节点上的任何程序进行交互。
简单的整数算术
咱们来计算一些算术表达式。
1> 2 + 3 * 4.
14
2> (2 + 3) * 4.
20
你会发现 Erlang 遵循算术表达式的常规规则,因此 2 + 3 * 4
表示 2 + (3 * 4)
而不是 (2 + 3) * 4
。
Erlang 使用任意大小的整数,执行整数运算。在 Erlang 中,整数运算是精确的,因此咱们不必担心运算溢出,或无法用特定字大小表示整数。
为什么不试试呢?咱们可以非常大数计算,给朋友留下深刻印象。
3> 123456789 * 987654321 * 112233445566778899 * 998877665544332211.
13669560260321809985966198898925761696613427909935341
咱们可以多种方法输入整数(详情请参阅 整数)。下面是一个使用基数 16 与基数 32 表示法的表达式:
4> 16#cafe * 32#sugar.
1577682511434
知识点:
- arbitrary-sized integers
- a certain word size
- Erlang 最高支持到 36 进制
变量
我们可将命令的结果,存储在某个变量中。
1> X = 123456789.
123456789
在第一行中,我们给了变量 X
一个值;在下一行中,shell 就会打印出该变量的值。
请注意,所有变量名字,都 必须 以一个大写字母开头。
若咱们要查看某个变量的值,只需输入变量名即可。
2> X.
123456789
现在 X
有了个值,咱们就可以使用他了。
3> X*X*X*X.
232305722798259244150093798251441
但是,若咱们尝试将一个不同值赋给变量 X
,咱们将得到一条错误消息。
4> X = 1234.
** exception error: no match of right hand side value 1234
单一赋值就如同代数
在我(作者)上学的时候,我的数学老师曾说:“如果在同一个等式中几个不同部分都有一个
X
,那么所有的X
都表示同一个意思”。这正是我们能解出方程的所在:如果我们知道X+Y=10
和X-Y=2
,那么在这两个等式中,X
将是 6,Y
将是 4。而当我(作者)学习第一门编程语言时,我们看到的就是这样的东西:
X = X + 1
大家都抗议,说:“你们不能这样做!” 但老师说我们错了,我们必须忘掉在数学课上学到的东西。
在 Erlang 中,变量就像他们在数学中一样。当咱们将某个值与某个变量关联时,咱们就是在做一个断言--一种事实的陈述。这个变量有了这个值。就是这样。
为解释这里发生的事情,我必须打破咱们对这个简单语句 X = 1234
的两个假设。
- 首先,
X
不是个变量,不是咱们在 Java 与 C 语言中,所习惯的那种变量; - 其次,
=
不是个赋值运算符;而是模式匹配运算符。
对 Erlang 新手来说,这可能是最棘手的问题之一,因此我们来深入探讨一下。
Erlang 变量不会变化
Erlang 有的是 单一赋值变量。顾名思义,变量只能赋值一次。若咱们试图在某个变量被设置后,更改他的值,那么咱们将得到一个报错(事实上,咱们将会得到我们刚看到的 badmatch 错误)。已被赋值的变量,称为 绑定 变量,否则被称为 未绑定 变量。
在 Erlang 看到诸如 X = 1234
这样的语句,且 X
之前未被绑定时,他就会将变量 X
绑定到值 1234
上。在绑定前,X
可以取任何值:他只是个等待填充的空槽。但是,他一旦得到了某个值,就会永远保留。
说到这里,咱们可能会想知道,为什么我们要使用 变量 这个名字。这有两个原因。
- 他们属于变量,不过其值只能更改一次(即他们会从未绑定,变为有个值);
- 他们看起来像传统编程语言中的变量,所以当我们看到一行这样开头的代码时:
X = ... %% '...' means 'Code I'm not showing'
然后我们的大脑就会说:“啊哈,我知道这是什么;X
是个变量,=
是个赋值运算符”。而我们的大脑几乎是正确的:X
几乎是个变量,而 =
几乎是个赋值运算符。
事实上,=
是个模式匹配运算符,当 X
是个未绑定变量时,他行为就类似于赋值。
最后,某个变量的 作用域,是他于其间被定义的某个词法单位,the scope of a variable is the lexical unit in which it is defined。因此,若 X
是在某个函数子句中用到,那么他的值就不会 “逃逸” 到这个子句外。同一个函数中不同子句之间,不存在共享的全局变量或私有变量。若 X
出现在许多不同的函数中,那么 X
的所有值都是不相关的。
变量绑定与模式匹配
在 Erlang 中,变量获得值是一次模式匹配运算成功的结果。
在大多数语言中,=
表示一个赋值语句。但在 Erlang 中,=
则是一次 模式匹配 运算。Lhs = Rhs
的真正意思是:计算出右侧 (Rhs
) ,然后将结果与左侧 (Lhs
) 的模式匹配。
某个变量,如 X
,是模式的一种简单形式。正如我们前面所说,变量只能被赋值一次。当我们 第一次 说 X = SomeExpression
时,Erlang 会对自己说:“我该怎么做才能让这条语句为真呢?” 因为 X
还没有一个值,所以他可以将 X
与 SomeExpression
的值绑定,该语句就变得有效了,大家都很高兴。
若稍后阶段我们又说 X = AnotherExpression
,那么只有在 SomeExpression
和 AnotherExpression
相同时,匹配才会成功。下面是这方面的一些示例:
1> X = (2+4).
6
在这条语句前,X
没有值,因此模式匹配会成功,X
被绑定到 6
。
2> Y = 10.
10
同样,Y
被绑定到 10
。
3> X = 6.
6
这与第 1 行有些许不同;在这个表达式被求值前,X
为 6
,因此匹配会成功,同时 shell 会打印出该表达式的值,即 6
。
4> X = Y.
** exception error: no match of right hand side value 10
在这个表达式求值前,X
为 6
,Y
为 10
。6
不等于 10
,因此会打印一条错误消息。
5> Y = 10.
10
模式匹配成功,因为 Y
是 10
。
6> Y = 4.
** exception error: no match of right hand side value 4
这会失败,因为 Y
为 10
。
在这个阶段,看起来我(作者)在故弄玄虚。=
左边的所有模式都只是变量,可以是绑定的,也可以是未绑定的,但正如我们稍后将看到的,我们可以构造任意复杂的模式,并用 =
操作符匹配他们。在我们引入了用于存储复合数据项的元组和列表后,我将回到这个主题。
没有副作用就意味着我们可以使我们的程序并行处理
可修改内存区域的专业术语,叫做 可变状态。Erlang 是门函数式编程语言,有着不可变状态。
在本书后面部分,我们将研究如何编程多核的 CPU,并会发现有着不可变状态的好处是巨大的。
若咱们使用 C 或 Java 等传统编程语言编程多核 CPU ,咱们就必须面对 共享内存 的问题。为避免破坏共享内存,在访问内存时其就必须上锁。访问共享内存的程序,在操作共享内存时不得崩溃。
在 Erlang 中,就没有可变状态,没有共享内存,进而也没有锁。这使得我们的程序很容易并行化。
单一赋值为何会提升我们的程序
在 Erlang 中,一个变量只是对某个值的引用 -- 在 Erlang 的实现中,一个绑定的变量是由一个到包含着值的存储区域的指针表示的。这个值不可更改。
我们不能更改变量这一事实极为重要,且不同于 C 或 Java 等命令式语言中变量的行为。
使用不可变变量会简化调试。要理解为什么会这样,我们必须问问自己何谓错误,以及错误是如何暴露自己的。
我们发现某个程序不正确的一种相当常见方法是,我们发现某个变量有了未预期的值。一旦我们知道了哪个变量不正确,我们只需检查程序,找到该变量被绑定之处。由于 Erlang 变量是不可变的,因此产生该变量的代码一定是错误的。在命令式语言中,变量可以被多次更改,因此变量被更改的每个地方,都可能是错误发生的地方。而在 Erlang 中,只有一处要查找。
到这里,咱们可能会想,没有 可变变量可怎么编程?在 Erlang 中我们要怎么表达 X = X + 1
这样的事情呢?Erlang 的方式是引入一个其名字未曾用过的新变量(比如 X1
),并写下 X1 = X + 1
。
浮点数
我们来试着用浮点数做一些算术运算。
1> 5/3.
1.6666666666666667
2> 5/3.0.
1.6666666666666667
第 1 行中该行末尾的数字是整数 3
。句点表示该表达式的结束,而不是个小数点。若我(作者)想在这里使用浮点数,就会写成 3.0
。
当咱们以 /
除两个整数时,结果会自动转换为浮点数;因此,5/3
会求值为 1.66666666666667
。
3> 4/2.
2.0
尽管 4
恰好能被 2
整除,但结果依然是个浮点数而不是整数。要从除法中得到整数结果,我们必须使用运算符 div
和 rem
。
4> 5 div 3.
1
5> 5 rem 3.
2
6> 4 div 2.
2
N div M
是 N
除以 M
并丢弃余数。而 N rem M
是 N
除以 M
后的余数。
Erlang 在内部使用 64 位的 IEEE 754-1985 浮点数,因此用到浮点数的程序,会遇到与浮点数相关的四舍五入或精度问题,咱们在 C 语言中也会遇到。
原子
在 Erlang 中,原子用于表示常量值。
若咱们习惯了 C 或 Java 中的枚举类型,或者 Scheme 或 Ruby 中的符号,那么咱们就已经用到过与原子非常相似的东西。
C 程序员将对使用符号常量,使程序自文档化的惯例熟悉。典型的 C 程序会在包含大量常量定义的包含文件中,定义一组全局常量;例如,可能会有个包含以下内容的 glob.h
文件:
#define OP_READ 1
#define OP_WRITE 2
#define OP_SEEK 3
...
#define RET_SUCCESS 223
...
使用此类符号常量的典型 C 代码, 可能如下:
#include "glob.h"
int ret;
ret = file_operation(OP_READ, buff);
if( ret == RET_SUCCESS ) { ... }
在 C 程序中,这些常量的值并不重要;这里他们之所以重要,只是因为他们都不相同,并可比较他们是否相等。这个程序的 Erlang 对应程序,可能是这样的:
Ret = file_operation(op_read, Buff),
if
Ret == ret_success ->
...
在 Erlang 中,原子是全局的,且无需使用宏定义或包含文件,即可实现。
假设我们打算编写一个处理一周天数的程序。为此,我们就会使用原子 monday
、tuesday
......,表示星期。
原子以小写字母开头,后跟一串字母数字字符或下划线 (_
) 或 at (@
) 符号,例如,red
、december
、cat
、meters
、yards
、joe@somehost
及 a_long_name
等。
原子也可以用单引号('
)括起来。在使用这种引号形式时,我们可创建以大写字母开头的原子(否则会被解释为变量),或包含非字母数字字符的原子,例如 'Monday'
、'Tuesday'
、'+'
、'*'
、'an atom with spaces'
。咱们甚至可以为那些不需要加引号的原子加引号,因此 'a'
与 a
的含义完全相同。在一些语言中,单引号与双引号的互换使用。但 Erlang 中的情况并非如此。单引号的使用如前所述;双引号则用于限定字符串字面值。
原子的值就只是这个原子。因此,若我们给出的命令只是一个原子,那么 Erlang shell 将打印该原子的值。
1> hello.
hello
谈论原子的值或整数的值似乎有点奇怪。但由于 Erlang 是门函数式编程语言,因此每个表达式都必须有个值。这包括了只是极其简单表达式的整数与原子。
元组
设想我们要将固定数量的项目,归为单个实体。为此,我们要用到 元组。我们可通过把我们打算表示的值,括在大括号中,并用逗号分隔他们,创建出元组。例如,若我们想要表示某人的姓名和身高,我们可使用 {joe, 1.82}
。这是个包含一个原子和一个浮点数的元组。
元组类似于 C 中的结构体,不同之处在于他们是匿名的。在 C 中,一个 point
类型的变量 P
可以声明如下:
struct point {
int x;
int y;
} P;
使用点运算符(.
),我们就可以访问 C 结构体中的字段。因此,要设置点中的 x
和 y
值,我们可以这样说:
P.x = 10; P.y = 45;
Erlang 没有类型声明,因此要创建一个 “点”,我们可以这样写:
P = {10, 45}
这会创建出一个元组,并将其绑定到变量 P
。与 C 的结构体不同,元组的字段没有名字。由于这个元组本身只包含两个整数,我们必须记住他的用途。为更容易记住元组的用途,常见使用原子作为元组第一个元素,描述这个元组所表示内容。因此,我们可以写下 {point, 10, 45}
,而不是 {10, 45}
,这就使得程序就更容易理解。这种标记元组的方式,并非一项语言的要求,而是一种推荐的编程风格。
元组可以嵌套。设想我们打算表示某人一些事实 -- 名字、身高、脚的大小和眼睛颜色。我们可以这样做:
2> Person = {person, {name, joe}, {height, 1.82}, {footsize, 42}, {eyecolor, brown}}.
{person,{name,joe},
{height,1.82},
{footsize,42},
{eyecolor,brown}}
译注:上面的行也可以写为:
4> Person = {person, {name, joe}, {height, 1.82},
{footsize, 42}, {eyecolor, brown}}.
{person,{name,joe},
{height,1.82},
{footsize,42},
{eyecolor,brown}}
请注意我们如何使用原子标识字段,以及(在 name
和 eyecolor
的情况下)为字段赋值。
创建元组
元组在我们声明他们时,自动创建出来,并在他们无法继续使用时销毁。
Erlang 使用垃圾回收器,回收所有未使用的内存,因此我们不必担心内存分配问题。
若我们在构建某个新元组时,使用了某个变量,那么这个新元组将共享由该变量引用的数据结构值。下面是个示例:
1> F = {firstname, joe}.
{firstname,joe}
2> L = {lastname, armstrong}.
{lastname,armstrong}
3> P = {person, F, L}.
{person,{firstname,joe},{lastname,armstrong}}
若我们试图创建一个带有未定义变量的数据结构,那么我们将得到一个报错。
1> {true, Q, 23, Costs}.
* 1:8: variable 'Q' is unbound
译注:原文为:
5> {true, Q, 23, Costs}.
** 1: variable 'Q' is unbound **
可见新版本的 Erlang/OTP 中,报错信息有改变和优化,精确到了行号与在行内的位置。
这意味着变量 Q
未定义。
提取元组中的值
早先我们曾说过,看起来像赋值语句的 =
其实不是个赋值语句,而是个模式匹配运算符。咱们可能想知道我们为什么要这么迂腐。事实证明,模式匹配是 Erlang 的基础,被用于许多不同的任务。他被用于从数据结构中提取值,也被用于函数内部的控制流,还用于并行程序中,当我们将消息发往某个进程时,选取要处理的消息。
在我们打算提取元组中某些值时,我们就可以使用模式匹配运算符 =
。
咱们来回到咱们表示某个点的元组。
1> Point = {point, 10, 45}.
{point,10,45}
设想我们打算把 Point
的字段,提取到 X
和 Y
两个变量中,我们会像下面这样完成:
2> {point, X, Y} = Point.
{point,10,45}
3> X.
10
4> Y.
45
在命令 2 中,X
会被绑定为 10
,Y
会被绑定为 45
。表达式 Lhs = Rhs
的值会被定义为 Rhs
,因此 shell 会打印 {point,10,45}
。
正如咱们所见,等号两侧的元组,必须有着同样数量的元素,且两侧的对应元素必须绑定到同一值。
现在设想我们输入了这样的东西:
5> {point, C, C} = Point.
** exception error: no match of right hand side value {point,10,45}
模式 {point, C, C}
不与 {point, 10, 45}
匹配,因为 C
不可能同时是 10
和 45
。因此,这个模式匹配会失败,系统就打印了一条报错消息。
下面是个其中模式 {point, C, C}
确实匹配的示例:
6> Point1 = {point, 25, 25}.
{point,25,25}
7> {point, C, C} = Point1.
{point,25,25}
8> C.
25
若我们有一个复杂的元组,那么我们可以通过编写一个与该元组形状(结构)相同,且在我们想要提取值处包含着未绑定变量的模式,提取该元组中的值。
为说明这点,我们将首先定义一个包含了复杂数据结构的变量 Person
。
1> Person = {person, {name, joe, armstrong}, {footsize, 42}}.
{person,{name,joe,armstrong},{footsize,42}}
现在我们将编写一个提取此人名字的模式。
2> {_, {_, Who, _}, _} = Person.
{person,{name,joe,armstrong},{footsize,42}}
最后我们将看看 Who
的值。
3> Who.
joe
请注意在前面这个示例中,我们把 _
写作了我们不感兴趣变量的占位符。符号 _
被称为 匿名变量。与常规变量不同,在同一模式中多次出现的 _
,不必绑定到同一个值。
列表
列表被用于存储任意数量的东西。通过将列表元素括在一对方括号中,并用逗号分隔他们,我们创建出一个列表。
设想我们打算表示一幅图画。若我们假定这幅图是由三角形和正方形组成,那么我们就将这幅图表示为一个列表。
1> Drawing = [{square, {10, 10}, 10}, {triangle, {15,10}, {25,10}, {30,40}}].
[{square,{10,10},10},{triangle,{15,10},{25,10},{30,40}}]
这个绘图列表中的每个元素,都是一些固定大小的元组(比如,{square, Point, Side}
或 {triange, Point1, Point2, Point3}
),但绘图本身,则可包含任意数量的事物,因此就一个列表来表示。
某个列表的单个元素,可以是任何类型,因此,比如我们就可以写出以下代码:
2> [1+7, hello, 2-2, {cost, apple, 30-20}, 3].
[8,hello,0,{cost,apple,10},3]
术语
我们把列表中的首个元素,称为该列表的 头。若咱们设想把列表的头去掉,那么剩下的就叫做列表的 尾巴。
比如,若我们有个列表 [1,2,3,4,5]
,那么这个列表的头就是整数 1
,而尾巴就是列表 [2,3,4,5]
。请注意,某个列表的头可以是任何东西,但某列表的尾巴,通常也是个列表。
访问某列表的头,是种非常高效的操作,因此几乎所有列表处理函数,都是从提取列表头开始,对该列表头进行处理,然后再处理列表尾巴。
定义列表
当 T
是个列表时,那么 [H|T]
也是个头为 H
、尾巴为 T
的列表。其中竖线(|
)把列表的头与其尾巴分开。 []
则是个空列表。
LISP 程序员注意:[H|T]
是个有着 CAR H
和 CDR T
的 CONS 单元。在某个模式中,这种语法会解包出 CAR 与 CDR。而在某个表达式中,其会构造出一个 CONS 单元。
每当我们使用 [...|T]
构造器,构造某个列表时,都应确保 T
是个列表。当其是个列表时,那么新列表将是 “良好格式的”。而若 T
不是个列表,那么新列表就被说成是个 “不良列表”。大多数库函数都假定了列表是良好格式的,进而对于不良列表不会工作。
知识点:
- unpacks the CAR and CDR
- properly formed list
- improper list
通过写下 [E1,E2,...,En|T]
,我们就可将多个元素,添加到 T
的开头。
因此,比如当我们一开始如下定义了 ThingsToBuy
时:
3> ThingsToBuy = [{apples, 10}, {pears, 6}, {milk, 3}].
[{apples,10},{pears,6},{milk,3}]
随后我们就可以通过写下如下代码,扩展这个列表:
4> ThingsToBuy1 = [{oranges, 4}, {newspaper, 1}|ThingsToBuy].
[{oranges,4},{newspaper,1},{apples,10},{pears,6},{milk,3}]
提取列表中的元素
与其他操作一样,我们可通过模式匹配操作,提取出某个列表中的元素。当我们有个非空列表 L
时,那么表达式 [X|Y] = L
,其中 X
和 Y
是非绑定变量,将把该列表的头提取到 X
中,把该列表的尾巴提取到 Y
中。
因此,设想我们在商店里,并且我们有着咱们的购物清单 ThingsToBuy1
-- 那么我们做的第一件事,就是把这个清单拆开为其首尾两部分。
5> [Buy1|ThingsToBuy2] = ThingsToBuy1.
[{oranges,4},{newspaper,1},{apples,10},{pears,6},{milk,3}]
此命令会以绑定 Buy1 = {oranges,4}
及 ThingsToBuy2 = [{newspaper,1}, {apples,10}, {pears,6}, {milk,3}]
成功。我们去买橘子,然后就可以提取接下来的几个项目。
6> [Buy2, Buy3|ThingsToBuy3] = ThingsToBuy2.
[{newspaper,1},{apples,10},{pears,6},{milk,3}]
此命令会以绑定 Buy2 = {newspaper, 1}
、Buy3 = {apples, 10}
及 ThingsToBuy3 = [{pears, 6}, {milk, 3}]
成功执行。
字符串
严格来说,Erlang 中没有字符串。要表示一个 Erlang 中的字符串,我们可选择将字符串表示为一个整数列表,或一个二进制值(有关二进制的讨论,请参见 7.1 小 节,二进制)。当某个字符串被表示为整数列表时,则该列表中的每个元素,都表示一个 Unicode 编码点。
我们可通过使用字符串字面值,创建出这样一个列表。所谓 字符串字面值,只是个以双引号("
)括起来的字符序列,例如,我们可以这样写:
1> Name = "Hello".
"Hello"
2> Hello = "你好".
[20320,22909]
译注:shell 之所以无法直接打印出
"你好"
字符串,是因为20320
不是可打印的 Latin1 字符编码。
"Hello"
只是代表该字符串中各个字符整数字符代码列表的简称。
请注意:在某些编程语言中,字符串既可以单引号,也可以双引号括起来。而在 Erlang 中,我们必须使用双引号。
在 shell 打印某个列表的值时,若该列表中的所有整数都表示了可打印字符,则会以字符串字面值形式打印;否则,他会以列表写法形式打印(有关字符集的问题,请参见 8.8 小节,字符集)。
3> [1,2,3].
[1,2,3]
4> [83,117,114,112,114,105,115,101].
"Surprise"
5> [1,83,117,114,112,114,105,115,101].
[1,83,117,114,112,114,105,115,101]
在表达式 3 中,列表 [1,2,3]
在未转换下就被打印了。这是因为 1
、2
和 3
均不是可打印字符。
在表达式 4 中,该列表中所有项目都是可打印字符,因此该列表打印被打印为一个字符串字面值。
表达式 5 与表达式 4 类似,只是该列表以 1
开头,而 1
不是个可打印字符。因此,该列表在未转换下即被打印。
我们无需知道哪个整数代表某个特定字符。我们可使用 “美元符语法” 达到此目的。例如,$a
实际上就是代表字符 a
的整数,以此类推。
6> I = $s.
115
7> [I-32, $u, $r, $p, $r, $i, $s, $e].
"Surprise"
在我们使用列表表示字符串时,列表中的单个整数表示 Unicode 字符。我们必须使用特殊语法,输入某些字符,并在我们打印该列表时,选取正确的格式约定。最好用个示例解释这一点。
1> X = "a\x{221e}b".
[97,8734,98]
2> io:format("~ts~n",[X]).
a∞b
在第 1 行中,我们创建了三个整数的一个列表。第一个整数 97
是字符 a
的 ASCII 和 Unicode 编码。\x{221e}
写法用于 输入十六进制整数(8734
),表示 Unicode 的 INFINITY (无穷大)字符。最后,98
是字符 b
的 ASCII 和 Unicode 编码。shell 会以列表记法打印这个列表([97,8734,98]
);这是因为 8734
不是个可打印的 Latin1 字符编码。在第 2 行中,我们使用了个格式化 I/O 语句,以使用这个无穷大字符的正确字符字形,打印出该字符串。
译注:以这种方式,我们可以打印出上面 UTF-8 编码的汉字
"你好"
字符串字面值。
8> io:format("~ts~n", [Hello]).
你好
ok
若 shell 将某个整数列表打印为字符串,而咱们确实希望他被打印为整数列表,那么咱们就必须使用一个格式化的写入语句,如下所示:
1> X = [97,98,99].
"abc"
2> io:format("~w~n", ["abc"]).
[97,98,99]
ok
知识点:
- a formatted I/O statement
- a formatted write statement
又来模式匹配
在本章的最后,我们再来看一次模式匹配。
下表列出了一些模式及术语的示例;这些模式中的所有变量,都假定为未绑定的。所谓 项目,只是某个 Erlang 的数据结构。表中标为 结果 的第三列,显示了模式是否与该项目匹配,在匹配时,变量绑定就会被创建出来。请通读这些示例,确保咱们真正理解他们。
模式 | = | 项目 | 结果 |
---|---|---|---|
{X,adb} | = | {123,abc} | 以 X = 123 成功 |
{X,Y,Z} | = | {222,def,"cat"} | 以 X = 123 、Y = def 及 Z = "cat" 成功 |
{X,Y} | = | {333, ghi, "cat"} | 失败 -- 两个元组形状不同 |
X | = | true | 以 X = true 成功 |
{X,Y,X} | = | {{abc,12},42,{abc,12}} | 以 X = {abc,12} 及 Y = 42 成功 |
{X,Y,X} | = | {{abc,12},42,true} | 失败 -- X 无法同时与 {abc,12} 和 true 匹配 |
[H|T] | = | [1,2,3,4,5] | 以 H = 1 及 T = [2,3,4,5] 成功 |
[H|T] | = | "cat" | 以 H = 99 及 T = "at" 成功 |
[A,B,C|T] | = | [a,b,c,d,e,f] | 以 A = a 、B = b 、C = c 及 T = [d,e,f] 成功 |
若咱们对这其中任何一个不确定,那么可尝试在 shell 中输入 Pattern = Term
表达式,看看会发生什么。
以下是个示例:
1> {X, abc} = {123, abc}.
{123,abc}
2> X.
123
3> f().
ok
4> {X,Y,Z} = {222, def, "cat"}.
{222,def,"cat"}
5> X.
222
6> Y.
def
请注意:其中命令 f()
告诉 shell 忘掉 他所有的绑定。在该命令后,所有变量都会成为未绑定的,因此第 4 行中的 X
,与第 1 及第 2 行中的 X
毫无关系。
现在,我们已经熟悉了这些基本数据类型,以及单一赋值和模式匹配的概念。我们可以加快步伐,看看如何定义模组与函数。我们将在下一章中完成此事。
练习
-
请快速浏览一下 Erlang Shell 下的命令编辑,然后测试并记住这些行编辑命令;
-
请在 shell 中输入
help()
命令。咱们将看到一个长的命令清单。请试试其中一些命令; -
请尝试使用一个元组表示一座房屋,并使用房屋列表表示一条街道。请确保咱们可打包和解包这种表示中的数据。