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 的基本詞彙確實稱為原子。