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]]
列表是很类似链接串列的结构,结构的构成符合下列性质:
- [] 是列表。
- [A,B,C,D,E, ... ] 是列表。
- 如果 B 是列表, [A|B] 也是列表。
- 如果 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).
撰写函数必须注意二点:
- 同一个函数的规则必须写在一起,最后一条规则以句号结尾,其他规则以分号结尾。
- 函数由上往下依序检查,因此,如果有一种参数样式必须在第 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 使用 true 和 false 二個原子代表真偽判斷的結果。
防卫式经常用在需要补充限制条件的地方,多半是帮箭号 ( -> ) 规则提供过滤条件。
真伪运算子
[编辑]Erlang 的真伪运算子分为二类,运算的结果皆为 true 或 false :
- 二元运算子: not, and, or, xor, andalso, orelse 。最后二项是做捷径求值判断。
- 比较运算子: ==, /=, >, <, >=, =< 。
真伪运算,与前面提到的变数赋值,是基于相同的基础:样式匹配。
样式匹配
[编辑]样式匹配,一般是指比较二个词汇的样式是否相同。 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 的例外模式分为三种类别:
- error :程式呼叫 error/1 或 error/2 ,或 fault/1 或 fault/2 等等,就会送出 error 类别的例外。
- exit :程式呼叫 exit/1 就会送出 exit 类别的例外。
- 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
注解
[编辑]- Erlang 或 Lisp 这些函数语言、与 Prolog ,都跟 λ 演算式 ( lambda-expression ) 不可脱离关系,并且与一、二阶叙述逻辑系统 ( first / second -order predicate logic system ) 很有密切的连系。
- 基本的词汇,以古典逻辑学 ( classic logics ) 的用语来说,称为原子 ( atom ) 。而在 Erlang 的基本词汇确实称为原子。