函数定义
以下章节更详细地描述了Erlang函数的语法。首先我来给函数的各个语法元素命名。接着将详细描述这些元素。
术语
考虑以下模块:[*]
程序 2.5
- -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。
- factorial(N) when N == 0 -> 1;
- factorial(N) when N > 0 -> N * factorial(N - 1).
注意对于以上示例,我们可以调换子句的顺序,即:
- factorial(N) when N > 0 -> N * factorial(N - 1);
- factorial(N) when N == 0 -> 1.
在这个示例中子句首部模式与保护式的组合可以唯一确定一个正确的子句。
保护式断言
保护式断言的完整集合如下:
保护式 | 成立条件 |
---|---|
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和算术表达式的组合也可以作为保护式。它们是:
- 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
项式比较
可以出现在保护式中的项式比较运算符如下:
运算符 | 描述 | 类型 |
---|---|---|
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保护式函数时);然后再进行比较。
为了进行比较,定义如下的偏序关系:
- number < atom < reference < port < pid < tuple < list
元组首先按大小排序,然后再按元素排序。列表的比较顺序是先头部,后尾部。
如果比较运算符的两个参数都是数值类型且运算符为coerce型,则如果一个参数是integer另一个是float,那么integer将被转换为float再进行比较。
exact类型的运算符则不做这样的转换。
因此5.0==1+4为真,而5.0=:=4+1为假。
保护函数子句示例:
- 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的第二个子句可以写成:
- factorial(N) when N > 0 ->
- N1 = N - 1,
- F1 = factorial(N1),
- N * F1.
在对序列求值的过程中,表达式的求值结果要么与一个模式进行匹配,要么被直接丢弃。将函数主体拆分为序列的原因有这么几条:
- 确保代码的顺序执行——函数主体中的表达式是依次求值的,而在嵌套的函数调用中的函数则可能以任意顺序执行。
- 增强代码可读性——将函数写成表达式序列可以令程序更清晰。
- (通过模式匹配)拆解函数的返回值。
- 重用函数调用的返回值。对函数返回值的多次重用的示例如下:
- good(X) ->
- Temp = lic(X),
- {cos(Temp), sin(Temp)}.
上面的写法比下面这么写要好:
- bad(X) ->
- {cos(lic(X)), sin(lic(X)}.
二者表达的是同一个含义。lic代表长而复杂的计算过程(Long and Involved Calculation),即那些计算代价高的函数。