.. include:: ../include/substitution.rst .. highlight:: erlang :linenothreshold: 3 ************** 第2章 串行编程 ************** :翻译: 连城 本章介绍用于编写串行Erlang程序的概念。我们首先讨论变量赋值的基本机制和如何实现控制流程。为此,我们要先了解一下\ **项式**\ 、\ **模式**\ 和\ **模式匹配**\ 。 项式 ==== Erlang中以下数据类型\ [#]_\ 被称为\ **项式**\ : - **常量**\ 类型 - 数值 - 整数,用于存储自然数 - 浮点数,用于存储实数 - 原子式 - Pid(进程标识符process identifier的缩写),用于存储进程标识 - 引用,用于存储系统范围内唯一的引用 - **复合**\ 数据类型 - 元组,用于存储固定数目的多个项式 - 列表,用于存储可变数目的多个项式 数值 ---- 以下实例都属于数值: .. code-block:: erlang 123 -34567 12.345 -27.45e-05 整数精度与实现相关,但任何Erlang系统都应保证至少24位的整数精度。 ``$``\ 标记表示字符\ ``Char``\ 对应的ASCII值,如\ ``$A``\ 表示整数65。 不以10为基数的整数可写作\ ``#``\ ,如16#ffff表示十进制整数65535。\ ``Base``\ 的取值范围为2 .. 16。 浮点数以传统方式书写。 原子式 ------ 原子式是有名称的常量。例如在某个用于日历计算的程序中可使用\ ``monday``\ 、\ ``tuesday``\ 等等表示一星期中的各天。原子式用于增强程序的可读性。 一些原子式实例: .. code-block:: erlang friday unquoted_atoms_cannot_contain_blanks 'A quoted atom which contains several blanks' 'hello \n my friend' 原子式以小写字母(\ ``a..z``\ )开头,以非字母数字字符结尾——否则就必须用引号括起来。 通过将原子式以引号括起来,原子式中便可以出现任意字符。原子式总是以可被 Erlang 读取程序读入的格式输出。原子式引号内的字符遵循如下规范: .. csv-table:: :header: 字符, 含义 :widths: 25, 75 ``\b`` , 退格符 ``\d`` , 删除符 ``\e`` , 转义符(ESC) ``\f`` , 换页符 ``\n`` , 换行符 ``\r`` , 回车符 ``\t`` , 制表符 ``\v`` , 垂直制表符 ``\\`` , 反斜线 ``\^A .. \^Z``\ ,control A到control Z(即0 .. 26) ``\'`` , 单引号 ``\"`` , 双引号 ``\OOO`` , 使用八进制格式\ ``OOO``\ 表示的字符 在引号括起来的原子式中如果包含字符序列\ ``\C``\ ,其中\ ``C``\ 的ASCII值小于32,则表示\ ``\C``\ 的这部分源码被忽略(这样我们在编程时就可以使用一个反斜线加换行符来将长原子式分隔为几行)。 元组 ---- 以花括号包围的一系列以逗号分隔的项式称为\ **元组**\ 。元组用于存储固定数目个项式。它们与传统编程语言中的\ **结构**\ 或\ **记录**\ 类似。 元组\ ``{E1,E2,...,En}``\ ,其中\ ``n``\ 大于0,称为\ **大小**\ 为\ ``n``\ 的元组。元组中的单个项式称为\ **元素**\ 。 以下是一些元组实例: .. code-block:: erlang {a, 12, 'hello'} {1, 2, {3, 4}, {a, {b, c}}} {} 列表 ---- 以方括号包围的一系列以逗号分隔的项式成为\ **列表**\ 。列表用于存储可变数目个项式。 对于列表\ ``[E1,E2,...En]``\ ,其中\ ``n`` >= 0 ,称其长度为\ ``n``\ 。 以下是一些元组实例: .. code-block:: erlang [1, abc, [12], 'foo bar'] [] [a,b,c] "abcd" 被我们称之为字符串的\ ``"..."``\ 标记,实际上是引号中各个字符组成的列表的ASCII简写形式。因此\ ``"abc"``\ 对应于\ ``[97,98,99]``\ 。在原子式中使用的转义规则在字符串中通用。 在对列表进行处理时,往往需要一种方便的手段来引用列表的第一个元素以及除掉第一个元素以外列表的剩余部分。方便起见,我们将列表的第一个元素称为\ **头部**\ ,将剩余部分称为 *尾部* 。 我们使用\ ``[E1,E2,E3,...,En|Variable]``\ 来标记一个前\ ``n``\ 个元素分别为\ ``E1,E2,E3,...,En``\ 而剩余部分记为\ ``Variable``\ 的列表。 注意“\ ``|``\ ”之后的项式不一定要是列表,它可以是任意一个合法的Erlang项式。最后一个尾部为项式\ ``[]``\ 的列表称为\ **真**\ 列表或\ **格式良好**\ 的列表——大多数(尽管不是全部)Erlang程序都是被编写来处理格式良好的列表的。 模式匹配 ======== 模式与项式有着相同的结构,但它们还可以包含变量。变量名都以大写字母开头。 模式示例: .. code-block:: erlang {A, a, 12, [12,34|{a}]} {A, B, 23} {x, {X_1}, 12, My_cats_age} [] 以上的\ ``A``\ 、\ ``B``\ 、\ ``X_1``\ 和\ ``My_cats_age``\ 都是变量。 模式匹配为变量赋值提供了基本的机制。被赋值后,变量便\ **被绑定**\ ——否则便是\ **未绑定**\ 变量。给变量赋值的动作称作“绑定”。变量一旦被绑定便不可更改。这种变量属性被称为\ **一次性绑定**\ 或\ **单次赋值**\ 。这种属性与传统命令式语言的\ **破坏性赋值**\ [#]_\ 相反。 如果一个\ **模式**\ 与一个\ **项式**\ 在结构上同构,且在模式中任一位置出现的原子数据类型也都在项式的相应位置上出现,则称他们它们相互\ **匹配**\ 。如果模式中包含未绑定变量,则该变量在匹配过程中将被绑定到项式中相应的元素。如果在模式中\ **相同的**\ 变量多次出现,则项式中对应位置的元素也必须相同。 模式匹配在以下情况下发生: - 计算形如\ ``Lhs = Rhs``\ 的表达式时 - 调用函数时 - 在\ ``case``\ 和\ ``receive``\ 原语中对指定模式进行匹配时 ``Pattern = Expression`` ------------------------ 表达式\ ``Pattern = Expression``\ 将致使\ ``Expression``\ 被求值,并将其结果与\ ``Pattern``\ 进行匹配。匹配要么成功要么失败。若匹配成功则\ ``Pattern``\ 中的所有(未绑定)变量都将被绑定。 以下我们将假设模式匹配总是\ **成功**\ 。对\ **失败**\ 的处理将在第??章详细讨论。 示例: .. code-block:: erlang {A, B} = {12, apple} 匹配成功后建立绑定关系\ ``A``\ |->|\ ``12``\ [#]_\ 和\ ``B``\ |->|\ ``apple``\ 。 .. code-block:: erlang {C, [Head|Tail]} = {{222, man}, [a,b,c]} 匹配成功后建立绑定关系\ ``C``\ |->|\ ``{222, man}``\ 、\ ``Head``\ |->|\ ``a``\ 和\ ``Tail``\ |->|\ ``[b, c]``\ 。 .. code-block:: erlang [{person, Name, Age, _}|T] = [{person, fred, 22, male}, {person, susan, 19, female}, ...] 匹配成功后建立绑定关系\ ``T``\ |->|\ ``[{person, susan, 19, female}, ...]}``\ 、\ ``Name``\ |->|\ ``fred``\ 和\ ``Age``\ |->|\ ``22``\ 。在最后一个例子中我们利用了写作“\ ``_``\ ”的\ **匿名**\ 变量——在语法上需要一个变量出现,但我们又不关心该变量的值的时候便可以使用匿名变量。 当一个变量在一个模式中多次出现时,只有被匹配的对应元素的值都相同时匹配才会成功。因此,举例来说,\ ``{A, foo, A} = {123, foo, 123}``\ 将成功匹配,并将\ ``A``\ 绑定到\ ``123``\ ,然而\ ``{A, foo, A} = {123, foo, abc}``\ 就会失败,因为我们不能将\ ``A``\ 同时绑定到\ ``123``\ **和**\ ``abc``\ 。 “\ ``=``\ ”是一个右结合的中缀运算符。因此\ ``A = B = C = D``\ 将被解析为\ ``A = (B = (C = D))``\ 。这种用法可能只有在\ ``{A, B} = X = ...``\ 这样的构造中才有用,这时我们可以同时获悉表达式的值及其组成部分。表达式\ ``Lhs = Rhs``\ 的值被定义为\ ``Rhs``\ 。 函数调用中的模式匹配 -------------------- Erlang通过模式匹配来提供选择和控制流程。例如,程序2.1定义了一个函数\ ``classify_day/1``\ ,当调用参数为\ ``saturday``\ 或\ ``sunday``\ 时返回\ ``weekEnd``\ ,否则返回\ ``weekDay`` 。 .. topic:: 程序 2.1 .. code-block:: erlang -module(dates). -export([classify_day/1]). classify_day(saturday) -> weekEnd; classify_day(sunday) -> weekEnd; classify_day(_) -> weekDay. 进行函数求值时,会将函数的参数与函数定义中出现的模式一一进行匹配。一旦发现一个成功的匹配,“\ ``->``\ ”之后的符号便被求值,因此: .. code-block:: erlang > dates:classify_day(saturday). weekEnd > dates:classify_day(friday). weekDay 如果所有的子句都不匹配,则函数调用\ **失败**\ (失败将引发第??章描述的错误捕捉机制)。 当执行流程进入一个函数的某个子句时,描述该子句的模式所包含的变量将被绑定。因此,举例来说,对程序1.3的\ ``math3:area({square, 5})``\ 进行求值将致使变量\ ``Side``\ 被绑定到\ ``5``\ 。 表达式求值 ========== 表达式具备与模式相同的语法,同时表达式还可以包含函数调用或传统的中序算术表达式。函数调用的写法很传统,如:\ ``area:triangle(A, B, C)``\ 便代表以参数\ ``A``\ 、\ ``B``\ 和\ ``C``\ 调用函数\ ``area:triangle``\ 。 Erlang 表达式的求值机制如下。 对项式求值得到其本身: .. code-block:: erlang > 222. 222 > abc. abc > 3.1415926. 3.14159 > {a,12,[b,c|d]}. {a,12,[b,c|d]} > {{},[{}],{a,45,'hello world'}}. {{},[{}],{a,45,'hello world'}} 浮点数的输出格式可能与它们的输入格式不完全一致。当表达式与项式同构且表达式中的函数调用都已求值完毕时,表达式将被求值为项式。应用一个函数时其参数首先被求值。 求值过程可以被认为是一个将表达式归结为基础项式的函数: .. parsed-literal:: |eps|\ (X) when Constant(X)\ |-->|\ X |eps|\ ({t1,t2,...,tn})\ |-->|\ {\ |eps|\ (t1),\ |eps|\ (t2),...,\ |eps|\ (tn)} |eps|\ ([t1,t2,...,tn])\ |-->|\ [\ |eps|\ (t1),\ |eps|\ (t2),...,\ |eps|\ (tn)] |eps|\ (functionName(t1,t2,...,tn)\ |-->| APPLY(functionName,[\ |eps|\ (t1),\ |eps|\ (t2),...,\ |eps|\ (tn)]) 其中\ ``APPLY``\ 表示一个将参数应用到函数的函数。 函数求值 -------- 函数调用的写法如以下实例所示: .. code-block:: erlang > length([a,b,c]). 3 > lists:append([a,b], [1,2,3]). [a,b,1,2,3] > math:pi(). 3.14159 带冒号形式的函数将在和模块相关的章节中解释。调用没有参数的函数必须加上一对空的小括号(以此与原子式相区别)。 求值顺序 -------- 函数参数的求值顺序是不确定的。例如,\ ``f({a},b(),g(a,h(b),{f,X}))``\ 表示一个函数调用。对函数\ ``f``\ 的调用有三个参数:\ ``{a}``\ 、\ ``b()``\ 和\ ``g(a,h(b),{f,X})``\ 。第一个参数是一个只包含一个原子项\ ``a``\ 的元组。第二个参数是一个函数调用\ ``b()``\ 。第三个参数是函数调用\ ``g(a,h(b),{f,X})``\ 。在对\ ``f/3``\ 求值时,对\ ``b/0``\ 和\ ``g/3``\ 的求值顺序是不确定的,不过\ ``h(b)``\ 在\ ``g/3``\ 被求值。对\ ``b()``\ 和\ ``h(b)``\ 的求值顺序也是不确定的。 在对形如\ ``[f(a), g(b), h(k)]``\ 的表达式进行求值时,\ ``f(a)``\ 、\ ``g(b)``\ 和\ ``h(k)``\ 的求值顺序是不确定的。 如果\ ``f(a)``\ 、\ ``g(b)``\ 和\ ``h(k)``\ 的求值过程没有副作用(即不发送消息、不创建进程等等),则\ ``[f(a), g(b), h(k)]``\ 的\ **值**\ 与求值顺序无关\ [#]_\ 。这种属性叫作\ **引用透明性**\ [#]_\ 。 应用 ---- BIF ``apply(Mod, Func, ArgList)``\ 和\ ``apply({Mod, Func}, ArgList)``\ 用于将模块\ ``Mod``\ 中的函数\ ``Func``\ 应用到参数列表\ ``ArgList``\ 。 .. code-block:: erlang > apply(dates, classify_day, [monday]). weekDay > apply(math, sqrt, [4]). 2.0 > apply({erlang, atom_to_list}, [abc]). [97,98,99] 使用\ ``apply``\ 对BIF进行求值时,可以使用\ ``erlang``\ 作为模块名。 模块系统 ======== Erlang具备一套模块系统以便我们将大型程序切分为一组模块。每个模块都有自己的名称空间;这样我们就可以在不同的模块中自由地使用相同的函数名而不会有任何冲突。 模块系统以对给定模块中函数的可见性进行限制的方式来工作的。函数的调用方式取决于模块名、函数名以及函数名是否在模块的导入或导出声明中出现。 .. topic:: 程序 2.2 .. code-block:: erlang -module(lists1). -export([reverse/1]). reverse(L) -> reverse(L, []). reverse([H|T], L) -> reverse(T, [H|L]); reverse([], L) -> L. 程序2.2定义了一个颠倒列表元素顺序的函数\ ``reverse/1``\ 。\ ``reverse/1``\ 是该模块中\ **唯一**\ 可以从该模块之外被调用的函数。需要从模块外部调用的函数必须出现在模块的导出声明中。 该模块中定义的其他函数,\ ``reverse/2``\ ,仅可供模块\ **内部**\ 使用。注意\ ``reverse/1``\ 和\ ``reverse/2``\ 是完全不同的函数。在Erlang中,名字相同但参数数目不同的两个函数是完全不同的函数。 模块间调用 ---------- 从其他模块中调用函数的方法有两种: .. topic:: 程序 2.3 .. code-block:: erlang -module(sort1). -export([reverse_sort/1, sort/1]). reverse_sort(L) -> lists1:reverse(sort(L)). sort(L) -> lists:sort(L). ``reverse/1``\ 以\ **完全限定名称**\ 被调用。 你还可以借助\ ``import``\ 声明使用\ **隐式限定函数名**\ ,如程序2.4所示。 .. topic:: 程序 2.4 .. code-block:: erlang -module(sort2). -import(lists1, [reverse/1]). -export([reverse_sort/1, sort/1]). reverse_sort(L) -> reverse(sort(L)). sort(L) -> lists:sort(L). 两种形式都是为了解决二义性。比如,当两个不同的模块导出了重名的函数,则必须显式限定函数名。 函数定义 ======== 以下章节更详细地描述了Erlang函数的语法。首先我来给函数的各个语法元素命名。接着将详细描述这些元素。 术语 ---- 考虑以下模块: .. topic:: 程序 2.5 .. code-block:: erlang -module(lists2). -export([flat_length/1]). %% flat_length(List) %% Calculate the length of a list of lists. flat_length(List) -> flat_length(List, 0). flat_length([H|T], N) when list(H) -> flat_length(H, flat_length(T, N)); flat_length([H|T], N) -> flat_length(T, N + 1); flat_length([], N) -> N. 以“\ ``%``\ ”打头的是注释。注释可以从一行的任意位置开始,一直持续到行末。 第1行包含\ **模块**\ 声明。该行必须出现在任何其他声明或代码之前。 第1行和第3行开头的“\ ``-``\ ”称为\ **属性前缀**\ 。\ ``module(list2)``\ 便是属性的一个例子。 第2、第4等行是空行——连续的单个或多个空白符、空行、制表符、换行符等,都被当作单个空白符处理。 第3行声明了一个具有一个参数的函数\ ``flag_length``\ ,该行意味着该函数存在于模块中并会被从模块中导出。 第5、6行是注释。 第8、9行包含了函数\ ``flat_length/1``\ 的定义。它由单个\ **子句**\ 组成。 表达式\ ``flat_length(List)``\ 称为子句的\ **头部**\ 。“\ ``->``\ ”之后的部分为子句的\ **主体**\ 。 第11至16行函数\ ``flat_length/2``\ 的定义——该函数包含三个子句;子句间以分号“\ ``;``\ ”分隔,在最后的结尾处以“\ ``.``\ ”结尾。 第11行中\ ``flat_length/2``\ 的第一个参数为列表\ ``[H|T]``\ 。\ ``H``\ 表示列表的\ **头部**\ ,\ ``T``\ 代表列表的\ **尾部**\ 。在关键字\ ``when``\ 和箭头“\ ``->``\ ”之间的表达式\ ``list(H)``\ 称作保护式。只有在参数与函数头部的模式相匹配且保护式断言成立时,函数体才会被求值。 ``flat_length/2``\ 的第一个子句称为\ **保护子句**\ ;其他的子句称为\ **无保护子句**\ 。 ``flat_length/2``\ 是一个\ **局部函数**\ ——即不可从模块外部被调用(因为它没有出现在\ ``export``\ 属性中)。 模块\ ``lists2``\ 包含了函数\ ``flat_length/1``\ **和**\ ``flat_length/2``\ 的定义。它们代表\ **两个完全不同的函数**\ ——这与C或Pascal等语言不通,在这些语言中一个函数名只能出现一次,且只能有\ **固定**\ 个数的参数。 子句 ---- 每个函数都由一组\ **子句**\ 组成。子句间以分号“\ ``;``\ ”分隔。每个子句都包含一个子句头部、一个可选的保护式和子句主体。下面将详细解释。 子句头部 -------- 子句的头部包含一个函数名和一组以逗号分隔的参数。每个参数都是一个合法的模式。 当函数调用发生时,将会按顺序对函数定义中的子句头部依次进行匹配。 子句保护式 ---------- 保护式是子句被选中前必须要满足的条件。 保护式可以是一个简单的断言或是一组由逗号分隔的简单断言。一个简单断言可以是一个算数比较、项式比较,或是一个系统预定义的断言函数。保护式可以看作是模式匹配的一种扩展。用户自定义的函数不能用在保护式内。 对保护式求值时所有的断言都将被求值。若所有断言都为真,则保护式成立,否则就失败。保护式中各个断言的求值顺序是不确定的。 如果保护式成立,则会对子句的主体进行求值。如果保护式失败,则尝试下一个候选子句。 一旦子句的头部和保护式都匹配成功,系统将\ **指定**\ 这条子句并对其主体求值。 我们可以写一个保护式版本的\ ``factorial``\ 。 .. code-block:: erlang factorial(N) when N == 0 -> 1; factorial(N) when N > 0 -> N * factorial(N - 1). 注意对于以上示例,我们可以调换子句的顺序,即: .. code-block:: erlang factorial(N) when N > 0 -> N * factorial(N - 1); factorial(N) when N == 0 -> 1. 在这个示例中子句首部模式与保护式的组合可以唯一确定一个正确的子句。 保护式断言 ---------- 保护式断言的完整集合如下: .. csv-table:: :header: 保护式, 成立条件 :widths: 40, 60 ``atom(X)``, ``X``\ 是一个原子式 ``constant(X)``, ``X``\ 不是列表或元组 ``float(X)``, ``X``\ 是一个浮点数 ``integer(X)``, ``X``\ 是一个整数 ``list(X)``, ``X``\ 是一个列表或 ``[]`` ``number``, ``X``\ 是一个整数或浮点数 ``pid(X)``, ``X``\ 是一个进程标识符 ``port(X)``, ``X``\ 是一个端口 ``reference(X)``, ``X``\ 是一个引用 ``tuple(X)``, ``X``\ 是一个元组 ``binary(X)``, ``X``\ 是一段二进制数据 另外,一些BIF和算术表达式的组合也可以作为保护式。它们是: .. code-block:: erlang element/2, float/1, hd/1, length/1, round/1, self/0, size/1 trunc/1, tl/1, abs/1, node/1, node/0, nodes/0 项式比较 -------- 可以出现在保护式中的项式比较运算符如下: .. csv-table:: :header: 运算符, 描述, 类型 :widths: 30, 40, 30 ``X > Y``, ``X``\ 大于\ ``Y``, coerce ``X < Y``, ``X``\ 小于\ ``Y``, coerce ``X =< Y``, ``X``\ 小于或等于\ ``Y``, coerce ``X >= Y``, ``X``\ 大于或等于\ ``Y``, coerce ``X == Y``, ``X``\ 等于\ ``Y``, coerce ``X /= Y``, ``X``\ 不等于\ ``Y``, coerce ``X =:= Y``, ``X``\ 等于\ ``Y``, exact ``X =/= Y``, ``X``\ 不等于\ ``Y``, exact 比较运算符工作机制如下:首先对运算符两边求值(如,在表达式两边存在算术表达式或包含BIF保护式函数时);然后再进行比较。 为了进行比较,定义如下的偏序关系: .. code-block:: erlang number < atom < reference < port < pid < tuple < list 元组首先按大小排序,然后再按元素排序。列表的比较顺序是先头部,后尾部。 .. 下文coerce和exact待翻译 如果比较运算符的两个参数都是数值类型且运算符为\ *coerce*\ 型,则如果一个参数是\ ``integer``\ 另一个是\ ``float``\ ,那么\ ``integer``\ 将被转换为\ ``float``\ 再进行比较。 ``exact``\ 类型的运算符则不做这样的转换。 因此\ ``5.0 == 1 + 4``\ 为真,而\ ``5.0 =:= 4 + 1``\ 为假。 保护函数子句示例: .. code-block:: erlang foo(X, Y, Z) when integer(X), integer(Y), integer(Z), X == Y + Z -> foo(X, Y, Z) when list(X), hd(X) == {Y, length(Z)} -> foo(X, Y, Z) when {X, Y, size(Z)} == {a, 12, X} -> foo(X) when list(X), hd(X) == c1, hd(tl(X)) == c2 -> 注意在保护式中不可引入新的变量。 子句主体 -------- 子句的主体有一个或多个有逗号分隔的表达式序列组成。序列中的表达式依次被求值。表达式序列的值被定义为序列中\ **最后一个**\ 表达式的值。例如,\ ``factorial``\ 的第二个子句可以写成: .. code-block:: erlang factorial(N) when N > 0 -> N1 = N - 1, F1 = factorial(N1), N * F1. 在对序列求值的过程中,表达式的求值结果要么与一个模式进行匹配,要么被直接丢弃。将函数主体拆分为序列的原因有这么几条: - 确保代码的顺序执行——函数主体中的表达式是依次求值的,而在嵌套的函数调用中的函数则可能以任意顺序执行。 - 增强代码可读性——将函数写成表达式序列可以令程序更清晰。 - (通过模式匹配)拆解函数的返回值。 - 重用函数调用的返回值。 对函数返回值的多次重用的示例如下: .. code-block:: erlang good(X) -> Temp = lic(X), {cos(Temp), sin(Temp)}. 上面的写法比下面这么写要好: .. code-block:: erlang bad(X) -> {cos(lic(X)), sin(lic(X)}. 二者表达的是同一个含义。\ ``lic``\ 代表长而复杂的计算过程(Long and Involved Calculation),即那些计算代价高的函数。 原语 ==== Erlang提供了元语\ ``case``\ 和\ ``if``\ ,这样在子句中无需借助其他函数便可以直接进行条件求值。 Case ---- ``case``\ 表达式允许在子句主体内部于多个选项中进行选择,语法如下: .. code-block:: erlang case Expr of Pattern1 [when Guard1] -> Seq1; Pattern2 [when Guard2] -> Seq2; ... PatternN [when GuardN] -> SeqN end 首先,对\ ``Expr``\ 求值,然后,\ ``Expr``\ 的值将依次与模式\ ``Pattern1``\ 、\ ``Pattern2``\ ……\ ``PatternN``\ 进行匹配,直到匹配成功。如果找到一个匹配并且(可选的)的保护式成立,则对应的调用序列将被求值。注意\ ``case``\ 保护式与函数保护式形式相同。\ ``case``\ 原语的值就是被选中的序列的值。 至少得有一个模式\ **必须**\ 得以匹配——否则就会产生一个运行时错误并引发第??章中的错误处理机制。 举个例子,比方说我们我有个函数\ ``allocate(Resource)``\ 用于分配某种资源\ ``Resource``\ 。假设这个函数只返回\ ``{yes, Address}``\ 或\ ``no``\ 。这样,这个函数便可以放在一个\ ``case``\ 结构里: .. code-block:: erlang ... case allocate(Resource) of {yes,Address} when Address > 0, Address =< Max -> Sequence 1 ... ; no -> Sequence 2 ... end ... 在\ ``Sequence 1 ...``\ 中,变量\ ``Address``\ 已经被绑定在了\ ``allocate/1``\ 的返回结果上。 为了避免匹配错误的发生,我们常常追加一个必会匹配的模式\ [#]_\ 作为\ ``case``\ 原语的最后一个分支: .. code-block:: erlang case Fn of ... _ -> true end If -- ``if``\ 表达式的语法如下: .. code-block:: erlang if Guard1 -> Sequence1 ; Guard2 -> Sequence2 ; ... end 在这种情况下,保护式\ ``Guard1,...``\ 将被依次求值。如果一个保护式成立则对与之关联的序列求值。该序列的求值结果便是\ ``if``\ 结构的结果。\ ``if``\ 保护式与函数保护式形式相同。与\ ``case``\ 相同,一个保护式都不成立的话将引发一个错误。如果需要,可以增加保护式断言\ ``true``\ 作为垃圾箱: .. code-block:: erlang if ... true -> true end Case 和 if 使用示例 ------------------- 使用\ ``case``\ 和\ ``if``\ 我们可以以多种方式来编写\ ``factorial``\ 。 最简单的: .. code-block:: erlang factorial(0) -> 1; factorial(N) -> N * factorial(N - 1). 使用函数保护式: .. code-block:: erlang factorial(0) -> 1; factorial(N) when N > 0 -> N * factorial(N - 1). 使用\ ``if``\ : .. code-block:: erlang factorial(N) -> if N == 0 -> 1; N > 0 -> N * factorial(N - 1) end. 使用\ ``case``\ : .. code-block:: erlang factorial(N) -> case N of 0 -> 1; N when N > 0 -> N * factorial(N - 1) end. 使用变量保持临时结果: .. code-block:: erlang factorial(0) -> 1; factorial(N) when N > 0 -> N1 = N - 1, F1 = factorial(N1), N * F1. 以上所有定义都是正确且等价的\ [#]_\ ——如何进行选择完全是个美学问题\ [#]_\ 。 算术表达式 ========== 算术表达式由以下运算符构成: .. csv-table:: :header: 运算符, 描述, 类型, 操作数类型, 优先级 :widths: 15, 50, 10, 15, 10 ``+ X``, ``+ X``, 单目, 混合, 1 ``- X``, ``- X``, 单目, 混合, 1 ``X * Y``, ``X * Y``, 双目, 混合, 2 ``X / Y``, ``X / Y``\ (浮点除法), 双目, 混合, 2 ``X div Y``, ``X``\ 整除\ ``Y``, 双目, 整数, 2 ``X rem Y``, ``X``\ 除以\ ``Y``\ 的余数, 双目, 整数, 2 ``X band Y``, ``X``\ 与\ ``Y``\ 的位与, 双目, 整数, 2 ``X + Y``, ``X + Y``, 双目, 混合, 3 ``X - Y``, ``X - Y``, 双目, 混合, 3 ``X bor Y``, ``X``\ 与\ ``Y``\ 位或, 双目, 整数, 3 ``X bxor Y``, ``X``\ 与\ ``Y``\ 的位算数异或, 双目, 整数, 3 ``X bsl N``, ``X``\ 算数左移\ ``N``\ 位, 双目, 整数, 3 ``X bsr N``, ``X``\ 右移\ ``N``\ 位, 双目, 整数, 3 **单目**\ 运算符有一个参数,\ **双目**\ 运算符有两个参数。\ **混合**\ 意味着参数即可以是\ ``integer``\ 也可以是\ ``float``\ 。单目运算符的返回值与其参数类型相同。 双目混合运算符(即\ ``*``\ 、\ ``-``\ 、\ ``+``\ )在参数都是\ ``integer``\ 时返回类型为\ ``integer``\ 的对象,在参数至少包含一个\ ``float``\ 时返回一个\ ``float``\ 。浮点除法运算符\ ``/``\ 总是返回一个\ ``float``\ 。 双目整数运算符(即\ ``band``\ 、\ ``div``\ 、\ ``rem``\ 、\ ``bor``\ 、\ ``bxor``\ 、\ ``bsl``\ 、\ ``bsr``\ )的参数必须是整数,其返回值也是整数。 求值顺序取决于运算符的优先级:首先计算第1优先级的运算符,然后是第2优先级,以此类推。括号内的表达式优先求值。 优先级相同的运算符从左到右进行求值。比如: .. code-block:: erlang A - B - C - D 其求值顺序与下面的表达式一致: .. code-block:: erlang (((A - B) - C) - D) 变量作用域 ========== 子句中变量的生存期从它首次被绑定处开始,到子句中对该变量的最后一个引用处结束。变量的绑定只会在模式匹配中发生;可以将之认作是一个变量产生过程。后续对变量的所有引用都是对变量的值的\ **使用**\ 。\ **表达式中的变量必须是经过绑定的**\ 。变量第一次出现时就被用在表达式中是非法的。比如: .. code-block:: erlang :linenos: f(X) -> Y = g(X), h(Y, X), p(Y). 第1行中,定义了变量\ ``X``\ (它在进入函数时被绑定)。第2行中,使用了\ ``X``\ ,定义了\ ``Y``\ (首次出现)。第3行中,使用了\ ``X``\ 和\ ``Y``\ ,然后在第4行中使用了\ ``Y``\ 。 ``if``\ 、\ ``case``\ 和\ ``receive``\ 的作用域规则 --------------------------------------------------- 在\ ``if``\ 、\ ``case``\ 或\ ``receive``\ 原语中引入的变量会被隐式导出到原语主体之外。比方我们有: .. code-block:: erlang f(X) -> case g(X) of true -> A = h(X); false -> A = k(X) end, ... 变量\ ``A``\ 在其被定义的\ ``case``\ 原语之后仍然有效。从\ ``if``\ 、\ ``case``\ 或\ ``receive``\ 原语中导出变量时应注意一些规则: **在**\ ``if``\ **、**\ ``case``\ **或**\ ``receive``\ **原语的不同分支中引入的变量集合必须相同,除非缺少的变量在原语外不再被引用。** 例如以下代码: .. code-block:: erlang f(X) -> case g(X) of true -> A = h(X), B = A + 7; false -> B = 6 end, h(A). 这段代码就是非法的。因为在对\ ``true``\ 分支求值时定义了变量\ ``A``\ 和\ ``B``\ ,而在对\ ``false``\ 分支求值时只定义了\ ``B``\ 。在\ ``case``\ 原语之后,又在调用\ ``h(A)``\ 中引用了\ ``A``\ ——如果是\ ``fase``\ 分支被求值,则\ ``A``\ 尚未被定义。注意如果调用的不是\ ``h(A)``\ 而是\ ``h(B)``\ 则这段代码就是合法的,因为\ ``B``\ 在\ ``case``\ 原语的两个分支中都有定义。 .. rubric:: 脚注 .. [#] 附录A给出了Erlang的形式语法。 .. [#] 许多人认为破坏性赋值会导致难以理解和易错的不清晰的程序。 .. [#] 标记\ ``Var``\ |->|\ ``Value``\ 表示变量\ ``Var``\ 的值为\ ``Value``\ 。 .. [#] 假设所有函数调用都结束。 .. [#] 即是说函数的\ **值**\ 与调用上下文无关。 .. [#] 有时被称为垃圾箱。 .. [#] 好吧,\ **几乎是**\ ——想想看\ ``factorial(-1)``\ ? .. [#] 如果不知道选哪个,选最漂亮的那个! .. vim:ft=rst ts=4 sw=4 fenc=utf-8 enc=utf-8 et