Erlang程式设计与问题解决/Erlang 语法

维基教科书,自由的教学读本


语法总览[编辑]

话说:“万事起头难”。进入本书最难的一章,我们要先尽可能认识 Erlang 全部的语法词汇。 Erlang 语法,跟它的祖宗 Prolog 和它的朋友 Lisp 、 Haskell 相仿,语言的结构自然是符合逻辑学的基础。1

原子[编辑]

写程式,要先用一些方式表达资料。我们可以自然的文字表达各种词汇,而这些词汇就是被处理的资料单位。 Erlang 基本词汇称为原子(atom)。2 基本的原子是以小写文字开头的任意长度连续文字。

hello

如果原子必须以大写字母或数字开头、或在原子中必须有空格,则使用单引号包含一段文字,单引号包含的文字也是一个原子。

'hello, world'
'Ouch!'
'123'

变数[编辑]

以大写开头的连续文字是变数。若以底线 ( _ ) 开头的连续文字,则是匿名变数。

Any
_anything

变数提供单次赋值。“赋值”意思就是把变数和一个数值绑在一起。等号 ( = ) 对右边的词汇取值,将值与左边对应的词汇绑在一起,然后传回成功赋值的结果。一个已经赋值的变数,再赋值会失败。

Greeting = 'hello, world'

数值[编辑]

由数字开头的词汇,若格式符合一般认定的整数或浮点数,就是数值资料。须注意,数值不是原子。

100
3.1416

字元[编辑]

字元就是整数值。 ASCII 字元数值只在 0 到 255 之间,于是,在许多情况, 0 到 255 之间的数值会被 Erlang 以字元处理。另外, Erlang 提供了以钱号 ( $ ) 开头标记的字母,表示字元。钱号之后接反斜线 ( \ ) 开头的三位八进制数字、或是反斜线后接小写 x ( \x ) 后接二位十六进制数字,可以表达可见或不可见字元。

$a
$(
$\127
$\130
$\xF0

列表[编辑]

在此先提到第一个 Erlang 资料结构:列表。列表是指一列 Erlang 词汇,包含的可能是原子,也可能是其他东西,也可以包含一些列表。

一般列表使用逗号分隔表示法。

[]
[hello, 'World', 101]

另外还可以使用头尾表示法。

[a|[]]
[hello|['World'|101]]

列表是很类似链接串列的结构,结构的构成符合下列性质:

  1. [] 是列表。
  2. [A,B,C,D,E, ... ] 是列表。
  3. 如果 B 是列表, [A|B] 也是列表。
  4. 如果 B 是列表, [A_1, A_2, A_3, ... A_n|B] 也是列表。

字串[编辑]

前面告诉我们,字元是整数值。于是,字串是一列整数值。一方面,字串可以使用常理所接受的表示法,以双引号包含任意文字;另一方面,在 Erlang 内部,字串是以整数列表来表达。例如,以下二者彼此相同。

"hello, world"
[104,101,108,108,111,44,32,119,111,114,108,100]
(h   e   l   l   o   
                     ,  space
                           w    o   r   l  d)

值组[编辑]

值组是 Erlang 除了列表之外的另一种基本资料结构,是将有限数目的词汇用曲括号 ( {} ) 包含,其中各项以逗号分隔。值组的用途,可以代表向量,或者可以代表键值配对,使用的意义随着应用场合而不同。

{}
{position, 1, 2}
{'Mary', love, 'John'}
練習
陣列是程序式語言中的基本資料類型。不過, Erlang 的詞彙中缺少陣列這一項。請說說看:
1. 一般的陣列,與 Erlang 的列表有什麼異同?
2. 請綜合上述 Erlang 詞彙,描述陣列結構。

函数[编辑]

函数是一组规则,将一些输入资料对应到一些输出资料。输入资料的可能范围称为定义域,输出资料的可能范围称为值域。这些是一般的函数数学定义。

Erlang 跟随这种方式塑造每个函数。每个函数必须是一群规则,彼此之间以分号 ( ; ) 分隔,最后以句号结尾。每一条规则必须有函数名称和参数列,后接箭头 ( -> ) 之后,描述函数的本体。一些例子在前章已经看过,在此重列一次。

sum([]) -> [];
sum([Num|Ns]) -> Num + sum(Ns).

square(N) -> N * N.

sum_square([]) -> [];
sum_square([Num|Ns]) -> square(Num) + sum_square(Ns).

撰写函数必须注意二点:

  1. 同一个函数的规则必须写在一起,最后一条规则以句号结尾,其他规则以分号结尾。
  2. 函数由上往下依序检查,因此,如果有一种参数样式必须在第 N 条规则符合检查,请确保它不会符合第 1 到 N-1 条规则的检查。
練習
平均分數等級 ( Grade Point Average, GPA ) 劃分為 A, B, C, D, F 五級。
下列 gpa/1 函數接受一個 0 到 100 之間的數字,則將分數轉換為 GPA 。不過,
程式有一點問題。請說說看程式有什麼問題,又,該怎麼調整?
    gpa( N ) when N >= 60 -> 'D';
    gpa( N ) when N >= 70 -> 'C';
    gpa( N ) when N >= 80 -> 'B';
    gpa( N ) when N >= 90 -> 'A';
    gpa( N ) when N >= 0 -> 'F'.

防卫式[编辑]

上述练习题中,每一条函数规则头部都多了一句 when ... 描述句,这句子称为防卫式 ( guard expression ) 。防卫式是一系列的表达式,以逗号 ( , ) 分隔;逗号在此代表“且” ( and ) 的意义。

Erlang 程式中,表達式之間的逗號 ( , ) 是「且」的意思。而表達式之間的
分號 ( ; ) 是「或」的意思。並且,「且」的優先權比「或」高。這樣的語法
是從 Prolog 繼承來的。

所以,函數規則與規則之間的分號就是「或」,意思是在眾多規則之間挑選一個。

防卫式是一系列以逗号分隔的表达式,并且,这些表达式运算的总结必须是 true 或 false 。

Erlang 沒有布爾邏輯 ( boolean ) 方面的基本詞彙,不過, Erlang 使用
truefalse 二個原子代表真偽判斷的結果。

防卫式经常用在需要补充限制条件的地方,多半是帮箭号 ( -> ) 规则提供过滤条件。

真伪运算子[编辑]

Erlang 的真伪运算子分为二类,运算的结果皆为 true 或 false :

  1. 二元运算子: not, and, or, xor, andalso, orelse 。最后二项是做捷径求值判断。
  2. 比较运算子: ==, /=, >, <, >=, =< 。

真伪运算,与前面提到的变数赋值,是基于相同的基础:样式匹配

样式匹配[编辑]

样式匹配,一般是指比较二个词汇的样式是否相同。 Erlang 在多种场合会用到样式匹配,使用场合可能是在函数呼叫时比较呼叫方与被呼叫方,变数赋值时比较等号的左方与右方,或是在几种多选一叙述段落中比较输入项与匹配项。

函数呼叫的样式匹配[编辑]

以 ACM 程式竞赛问题集第 100 号问题 ( 3n+1 Problem ) 为例,求其中一解的程式为

solve(1) -> [1];
solve(N) when N rem 2 == 0 ->
    [N|solve(N div 2)];
solve(N) ->
    [N|solve(N*3+1)].

% rem 是餘數運算子, div 是商數運算子。 div 也就是所謂整數除法,除法後取整數解。

呼叫 solve(22) 时,对上述三句,第一次符合比对的是第二句,无论函数名称、参数数目、参数的样式、以及防卫式的评估结果,都符合。于是,它变成求解 [22|solve(22 div 2)] 。如果执行 solve(1) 会符合第一句而结束。然而,当执行 solve(0) 或 solve(-1) 时,不会如预期结果。修正的方式是加强第二、三句的防卫式,

solve(1) -> [1];
solve(N) when N > 0, N rem 2 == 0 ->
    [N|solve(N div 2)];
solve(N) when N > 0 ->
    [N|solve(N*3+1)].

于是, solve(0) 或 solve(-1) 找不到匹配的规则。

变数赋值的样式匹配[编辑]

在等号 ( = ) 左右边各放一个词汇,这样叙述的目的是要把右边的值绑在左边的变数。一般的形式是,左项为变数,右项为原子、数值、函数计算值、或已赋值的变数。

Length = length(solve(22))

而且变数赋值的处理范围不只如此。当有一个等号夹在二个词汇中间时,其意义是,先比对二个词汇的样式是否符合,如果符合,就按照等号左边词汇的子项,往等号右边的对应子项取值。最后,如果赋值成功,就传回赋值后的词汇。

所以,

Greeting = "hello, world"

% Greeting 成功賦值,結果是 "hello, world"
[H|T] = solve(22)

% H 和 T 成功賦值,全部結果是 [22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1] 。
% H 賦值為 22 , T 賦值為 [11,34,17,52,26,13,40,20,10,5,16,8,4,2,1] 。

[H1|_discarded] = [H|T]

% H1 成功賦值, _discarded 也成功賦值但已拋棄,全部結果同 [H|T] ,
% 而 H1 賦值為 22 。
[H2,H3|Rest] = T

% H2 、 H3 、 Rest 成功賦值,全部結果同 T 。
{hello, world} = {hello, world}

% 如此也賦值成功。

赋值失败的例子,有

{hello, world} = {A, world}

% A 沒有賦值,使全部賦值失敗。
{A, B} = [H|T].

% 之前 [H|T] 雖然已賦值,但本次賦值詞彙語法不同,賦值失敗。

选择叙述赋值[编辑]

接下来的几节要介绍几种 Erlang 的多选一叙述段落,原则也是根据匹配的项目,执行对应的程式。详细例子请见以下“因果式”、“案例式”、“试误”、“等候”等小节。

因果式[编辑]

Erlang 用 if ... end 处理“若、则、否则”的情况。 if ... end 语法是

if
    防衛式 -> 程式段落 ;
    防衛式 -> 程式段落 ;
    ......           ;
    防衛式 -> 程式段落
end

段落中有多项条件与结果的对应规则。最后一行防卫式为 true ,处理全部剩余的情况。

求 GPA 的例子可以用 if ... end 叙述完成。最后一行防卫式写为 true ,是处理全部的剩余情况。输入的分数 G 由上向下挑选第一条匹配的防卫式,执行对应的程式段落。

gpa(G) ->
    if
        G >= 0, G =< 100 ->
            if
                G >= 90 -> $A;
                G >= 80 -> $B;
                G >= 70 -> $C;
                G >= 60 -> $D;
                true -> $F
            end
    end.

案例式[编辑]

有时要根据一个词汇的情况,做不同的事情:例如,写一个 parser 或 evaluator 。以下,举一个四则计算程式为例。

eval(Expr) ->
    case Expr of
        {E1, '*', E2} ->
            eval(E1) * eval(E2);
        {E1, '/', E2} ->
            eval(E1) / eval(E2);
        {E1, '+', E2} ->
            eval(E1) + eval(E2);
        {E1, '-', E2} ->
            eval(E1) - eval(E2);
        Value ->
            Value
    end.

% 執行 eval({{3, '*', {4,'+',5}},'-',2}).
% 得 25

case ... end 的语法为

case 詞彙 of
    樣式 -> 程式段落 ;
    樣式 -> 程式段落 ;
    ......          ;
    樣式 -> 程式段落 ;
end

% 樣式 (expr) 之後可以附加以 when 陳述的防衛式 (guard)。

词汇跟各种样式比较,找到第一个匹配的样式,就执行对应的程式段落。

试误[编辑]

Erlang 提供 try ... end 叙述段落处理错误。首先该了解 Erlang 如何定义错误,以及如何补捉。

首先,在命令列使用 catch 关键字,可以对各种式子补捉例外。

> catch 10/0.
{'EXIT',{badarith,[{erlang,'/',[10,0]},
                   {erl_eval,do_apply,5},
                   {erl_eval,expr,5},
                   {shell,exprs,7},
                   {shell,eval_exprs,7},
                   {shell,eval_loop,3}]}}
> catch false=true.
{'EXIT',{{badmatch,true},[{erl_eval,expr,3}]}}

由此可见到,例外讯息的格式是 {'EXIT',{ 理由 , 回溯 }} 。另外还有其他的例外类型。

Erlang 的例外模式分为三种类别:

  1. error :程式呼叫 error/1 或 error/2 ,或 fault/1 或 fault/2 等等,就会送出 error 类别的例外。
  2. exit :程式呼叫 exit/1 就会送出 exit 类别的例外。
  3. throw :程式呼叫 throw/1 就会送出 throw 类别的例外。

于是, Erlang 提供了 try ... end 叙述,可以在程式中定义试误的过程。 try 的语法是:

try 詞彙 of
    樣式 -> 程式段落 ;
    樣式 -> 程式段落 ;
    ......          ;
    樣式 -> 程式段落
catch
    例外類別:例外樣式 -> 程式段落 ;
    例外類別:例外樣式 -> 程式段落 ;
    ......                     ;
    例外類別:例外樣式 -> 程式段落 ;
after
   程式段落
end

% after 段落保證在例外發生之後,一定會執行 after 的程式;即使 catch 部份再發生例外,
% after 部份仍會執行。

% 在樣式和例外樣式之後,可以接防衛式。
% of 、 catch 、 after 可省略,只寫其中之一:例如,至少只給一條 catch 規則。

接着,透过一个简单的例外揭示程式,我们看看例外补捉的内容。

-module(test).
-compile(export_all).

error() ->
    test = false.

exit() ->
    exit('nothing').

throw() ->
    throw('hello').

testm(F) ->
    try
	F()
    catch
	Class:Term ->
	    io:format("{~w, ~w}~n", [Class, Term])
    end.

error/0 、 exit/0 、 throw/0 分别做触发三种例外的工作。 testm/1 接受一个函数,就执行函数并补捉错误,将例外的类别与内容以格式化标准输出函数 io:format/2 印出成类似原讯息的样子。以下是执行情况:

> test:testm(fun test:error/0).
{error, {badmatch,false}}
ok
> test:testm(fun test:exit/0).
{exit, nothing}
ok
> test:testm(fun test:throw/0).
{throw, hello}
ok

% 參數的 fun test:error/0 這種標示,是表達一個函數。

Erlang 的程式设计哲学是:做最少该做的事,并且,在尽可能小的范围揭露错误。关于试误叙述,通常不会在程式到处放 try ... end 叙述,而是在很小的函数里做一下 try ... end 。

等候[编辑]

Erlang 语言的主要特征,是“平行导向程式设计”,语言上描述行程及行程之间的通讯。于是, Erlang 有送讯息和收讯息二种语法及关键字,并且有特定的函数可以建立行程,以及用变数代表行程。行程与讯息,请阅读“平行导向程式设计”章。

Erlang 的函数可以用 receive ... end 叙述段落描述本行程如何接收讯息。 receive ... end 语法为

receive
    樣式 -> 程式段落 ;
    樣式 -> 程式段落 ;
    ......          ;
    樣式 -> 程式段落
after
    毫秒數目 -> 程式段落
end

这说明本函数在行程方面,可以接受符合多种样式之一的讯息,匹配之后就执行对应的程式段落。在 receive 段落,行程是进入等待讯息的阶段。 after 段落描述本行程等待了指定的时间之后,要执行指定的程式段落。所以,以下的程式示范等待 N 秒就结束。

wait(N) ->
    io:format("waiting ..."),
    receive
    after
        N * 1000 -> io:foramt(" done~n")
    end.

其他运算符号[编辑]

在本节,补充其他 Erlang 语言特征。

逗号与分号[编辑]

Erlang 程式段落是由几句式子构成,式子之间会看到逗号 ( , ) 或分号 ( ; ) ,以及句号 ( . ) 。一句完整的程式段落是以句号结尾:例如,前面看到的模组设定句,以及函数定义式子。

-module(module_1).
-compile(export_all).

wyiiwyg(Any) -> Any.

逗号代表“且” ( and ) ,所以,程式段落和防卫式都可以是用逗号分隔很多句子。

分号代表“或” ( or ) 。同一函数的规则之间以分号分隔。前面提到的 if .. end 、 case ... end 、 try ... end 、和 receive ... end 等等,许多条样式判断规则之间也是用分号分隔。

并且,“且”比“或”有较高优先权,逗号比分号有较高优先权。

数值计算[编辑]

Erlang 提供 + 、 - 、 * 、 / 、 div 、 rem 等运算符号,可以做一般的数值计算。除法方面, / 是实数除法,计算之后保留小数。 div 是除法之后取商,即所谓整数除法,也就是将计算结果的小数部分丢掉。 rem 是除法之后取余数。

数值上有内建函数 is_integer/1 可以判断参数是否为整数, is_float/1 可以判断参数是否带有小数部分,并且有 is_number/1 可以判断参数是否为数值。

列表运算[编辑]

列表可用 [ ... | ... ] 格式建立,而这就是基本的列表建构运算符号。

[1,2,3|[]]
[1|0]

% 以上皆是列表,雖然在內容方面,第一項是合理的列表,第二項是不合理的列表。

Erlang 有内建函数 length/1 可以求一个列表的长度。而 length/1 只处理合理的列表。

> length([1|[]]).
1
> length([1|0]).
** exception error: bad argument
......

Erlang 有内建 lists 模组,其中有一个 seq/2 函数,可以产生二个整数之间由小到大的序列。

> lists:seq(1,10).
[1,2,3,4,5,6,7,8,9,10]
> lists:seq(3,2).
[]

另外,由集合论借来的集合建构格式 ( set builder / set comprehension ) , Erlang 还有一种列表型示 ( list comprehension ) 表示法。

even(List) ->
    [ N || N <- List, N rem 2 == 0 ].

列表型示的语法为

[ 將取出的單元做任意轉換 || 某列表的每一單元 <- 某列表 , 防衛式 ]

左箭头 ( <- ) 意思是抽取元素。在列表型示 ( list comprehension ) 右段是以一个变数代表每一个被抽出的元素,使得能在此式的左段建构为新的单位。

> [ N rem 2 == 0 || N <- lists:seq(1,6) ].
[false,true,false,true,false,true]

列表有二种运算子:串接运算子 ++ 和消除运算子 -- 。

串接运算子是将二个列表前后衔接。

> [1,2,3] ++ [4,5,6,7].
[1,2,3,4,5,6,7]

串接运算 ( ++ ) 与下列程式工作相等:

append([], Ys) -> Ys;
append([X|Xs], Ys) -> [ X | append(Xs, Ys) ].

所以,在 ++ 左端的计算量,是效能消耗的关键。使用上需斟酌。

消除运算子,是在左端列表中,将存在的右端列表项目一一扣除。

> [1,1,2,2,3,3] -- [3,2,2,1].
[1,3]

二进制资料[编辑]

为了满足一般的程式需求, Erlang 提供了描述二进制资料流的语法,可以从语言对应到系统资料的细节内容。

<< 數值 , 數值 , ... >>

在此暂时忽略细节。欲知细节请阅读维基百科 Erlang 条目、 Erlang.org 参考手册、以及《Erlang程式设计》一书。

λ 演算[编辑]

Erlang 可以表达 λ 表示法 ( lambda-expression ) 。 λ 表示法是 Church, Alonzo 发明的数学语言,比各种程式语言较来得早出现,以一种语法格式表达各种函数。

λ 輸入項 . 詞彙

λ 词汇中可以存在输入项,于是,将输入项当做参数,做一些转换。在 Erlang 可以用类似的语法,

fun ( 參數 , 參數 , ... ) -> 程式段落 end

这种表示法即所谓匿名函数。

在此以一个递回 λ 算式做为这个部分的开头示范。一个将指定数值 N ( N >= 1 ) 从 1 到 N 加总,可能先写成

> F = fun(N) -> if N == 1 -> 1; N > 1 -> N + F(N-1) end end.
* 1: variable 'F' is unbound

在此有个递回定义的问题:我还想要定义 F 函数,但在定义好 F 函数之前,却要先用到 F 。它的回应表明了这个问题。

有些数学先生或数学同学会告诉你:

F 1 -> 1
F N -> F(F N) 

所以适当的写法是

> F = fun(F, N) -> if N == 1 -> 1; N > 1 -> N + F(F, N-1) end end.
#Fun<erl_eval........>
> F(F, 10).
55

λ 算式的另一种用法,是用做用完即抛的函数。摆放位置与一般函数用法雷同。

> (fun(A)-> A end)(100).
100

注解[编辑]

  1. Erlang 或 Lisp 这些函数语言、与 Prolog ,都跟 λ 演算式 ( lambda-expression ) 不可脱离关系,并且与一、二阶叙述逻辑系统 ( first / second -order predicate logic system ) 很有密切的连系。
  2. 基本的词汇,以古典逻辑学 ( classic logics ) 的用语来说,称为原子 ( atom ) 。而在 Erlang 的基本词汇确实称为原子。