跳至內容

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