基本概念

本章介绍 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 系统上,使用 erlwerl 启动 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=10X-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 还没有一个值,所以他可以将 XSomeExpression 的值绑定,该语句就变得有效了,大家都很高兴。

若稍后阶段我们又说 X = AnotherExpression,那么只有在 SomeExpressionAnotherExpression 相同时,匹配才会成功。下面是这方面的一些示例:

1> X = (2+4).
6

在这条语句前,X 没有值,因此模式匹配会成功,X 被绑定到 6

2> Y = 10.
10

同样,Y 被绑定到 10

3> X = 6.
6

这与第 1 行有些许不同;在这个表达式被求值前,X6,因此匹配会成功,同时 shell 会打印出该表达式的值,即 6

4> X = Y.
** exception error: no match of right hand side value 10

在这个表达式求值前,X6Y106 不等于 10,因此会打印一条错误消息。

5> Y = 10.
10

模式匹配成功,因为 Y10

6> Y = 4.
** exception error: no match of right hand side value 4

这会失败,因为 Y10

在这个阶段,看起来我(作者)在故弄玄虚。= 左边的所有模式都只是变量,可以是绑定的,也可以是未绑定的,但正如我们稍后将看到的,我们可以构造任意复杂的模式,并用 = 操作符匹配他们。在我们引入了用于存储复合数据项的元组和列表后,我将回到这个主题。

没有副作用就意味着我们可以使我们的程序并行处理

可修改内存区域的专业术语,叫做 可变状态。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 整除,但结果依然是个浮点数而不是整数。要从除法中得到整数结果,我们必须使用运算符 divrem

4> 5 div 3.
1
5> 5 rem 3.
2
6> 4 div 2.
2

N div MN 除以 M 并丢弃余数。而 N rem MN 除以 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 中,原子是全局的,且无需使用宏定义或包含文件,即可实现。

假设我们打算编写一个处理一周天数的程序。为此,我们就会使用原子 mondaytuesday......,表示星期。

原子以小写字母开头,后跟一串字母数字字符或下划线 (_) 或 at (@) 符号,例如,reddecembercatmetersyardsjoe@somehosta_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 结构体中的字段。因此,要设置点中的 xy 值,我们可以这样说:

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}}

请注意我们如何使用原子标识字段,以及(在 nameeyecolor 的情况下)为字段赋值。

创建元组

元组在我们声明他们时,自动创建出来,并在他们无法继续使用时销毁。

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 的字段,提取到 XY 两个变量中,我们会像下面这样完成:

2> {point, X, Y} = Point.
{point,10,45}
3> X.
10
4> Y.
45

在命令 2 中,X 会被绑定为 10Y 会被绑定为 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 不可能同时是 1045。因此,这个模式匹配会失败,系统就打印了一条报错消息。

下面是个其中模式 {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,其中 XY 是非绑定变量,将把该列表的头提取到 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] 在未转换下就被打印了。这是因为 123 均不是可打印字符。

在表达式 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 = 123Y = defZ = "cat" 成功
{X,Y}={333, ghi, "cat"}失败 -- 两个元组形状不同
X=trueX = 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 = 1T = [2,3,4,5] 成功
[H|T]="cat"H = 99T = "at" 成功
[A,B,C|T]=[a,b,c,d,e,f]A = aB = bC = cT = [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 毫无关系。

现在,我们已经熟悉了这些基本数据类型,以及单一赋值和模式匹配的概念。我们可以加快步伐,看看如何定义模组与函数。我们将在下一章中完成此事。

练习

  1. 请快速浏览一下 Erlang Shell 下的命令编辑,然后测试并记住这些行编辑命令;

  2. 请在 shell 中输入 help() 命令。咱们将看到一个长的命令清单。请试试其中一些命令;

  3. 请尝试使用一个元组表示一座房屋,并使用房屋列表表示一条街道。请确保咱们可打包和解包这种表示中的数据。

Last change: 2025-08-29, commit: 894f495

小额打赏,赞助 xfoss.com 长存......

微信 | 支付宝

若这里内容有帮助到你,请选择上述方式向 xfoss.com 捐赠。