檢視原始碼 循序程式設計

Erlang Shell

大多數作業系統都有命令直譯器或 Shell,UNIX 和 Linux 有許多種,Windows 有命令提示字元、PowerShell 等。Erlang 有自己的 Shell,可以直接在其中撰寫 Erlang 程式碼片段,並評估結果(請參閱 STDLIB 中的 shell 手冊頁面)。

在您的作業系統中啟動 Shell 或命令直譯器,然後輸入 erl,即可啟動 Erlang Shell(在 Linux 或 UNIX 中)。您會看到類似以下的內容。

$ erl
Erlang R15B (erts-5.9.1) [source] [smp:8:8] [rq:8] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.9.1  (abort with ^G)
1>

在 Shell 中輸入 2 + 5.,然後按下 Enter 鍵(歸位)。請注意,您必須以句點 . 和歸位符號結束,才能告訴 Shell 您已完成輸入程式碼。

1> 2 + 5.
7
2> 

如所示,Erlang Shell 會為可輸入的行編號(如 1> 2>),並正確顯示 2 + 5 的結果是 7。如果您在 Shell 中輸入錯誤,可以使用退格鍵刪除,就像在大多數 Shell 中一樣。Shell 中還有更多編輯命令(請參閱 ERTS 使用者指南中的 tty - 命令列介面)。

(請注意,以下範例中 Shell 給出的許多行號是亂序的。這是因為本教學是在不同的階段編寫和程式碼測試的。)

這裡有一個更複雜的計算

2> (42 + 77) * 66 / 3.
2618.0

請注意括號、乘法運算子 * 和除法運算子 / 的使用方式,與一般算術相同(請參閱 運算式)。

按下 Control-C 即可關閉 Erlang 系統和 Erlang Shell。

將顯示以下輸出

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
a
$

輸入 a 即可離開 Erlang 系統。

關閉 Erlang 系統的另一種方法是輸入 halt/0

3> halt().
$

模組和函式

如果只能從 Shell 執行程式碼,那麼程式語言就沒什麼用處了。所以這裡有一個小的 Erlang 程式。使用合適的文字編輯器將其輸入到名為 tut.erl 的檔案中。檔案名稱 tut.erl 很重要,而且它必須與您啟動 erl 的目錄相同。如果幸運的話,您的編輯器會有 Erlang 模式,可以讓您更輕鬆地輸入和格式化程式碼(請參閱工具使用者指南中的 Emacs 的 Erlang 模式),但沒有它也可以順利操作。以下是要輸入的程式碼

-module(tut).
-export([double/1]).

double(X) ->
    2 * X.

不難猜到這個程式會將數字的值加倍。程式碼的前兩行稍後會說明。讓我們編譯這個程式。這可以在 Erlang Shell 中完成,如下所示,其中 c 表示編譯

3> c(tut).
{ok,tut}

{ok,tut} 表示編譯正常。如果出現 error,表示您輸入的文字中有錯誤。其他錯誤訊息會提供錯誤的原因,因此您可以修改文字,然後再次嘗試編譯程式。

現在執行程式

4> tut:double(10).
20

如預期,10 的雙倍是 20。

現在讓我們回到程式碼的前兩行。Erlang 程式寫在檔案中。每個檔案都包含一個 Erlang *模組*。模組中的第一行程式碼是模組名稱(請參閱 模組

-module(tut).

因此,模組的名稱是 *tut*。請注意行尾的句點 .。用於儲存模組的檔案名稱必須與模組名稱相同,但副檔名為 .erl。在此範例中,檔案名稱為 tut.erl。當使用另一個模組中的函式時,會使用 module_name:function_name(arguments) 的語法。因此,以下表示使用引數 10 呼叫模組 tut 中的函式 double

4> tut:double(10).

第二行表示模組 tut 包含一個名為 double 的函式,該函式接受一個引數(在我們的範例中是 X

-export([double/1]).

第二行也表示這個函式可以從模組 tut 外部呼叫。稍後將詳細說明。再次注意行尾的 .

現在來看一個更複雜的範例,一個數字的階乘。例如,4 的階乘是 4 * 3 * 2 * 1,等於 24。

將以下程式碼輸入到名為 tut1.erl 的檔案中

-module(tut1).
-export([fac/1]).

fac(1) ->
    1;
fac(N) ->
    N * fac(N - 1).

這是一個名為 tut1 的模組,其中包含一個名為 fac> 的函式,該函式接受一個引數 N

第一部分表示 1 的階乘是 1。

fac(1) ->
    1;

請注意,這部分以分號 ; 結尾,表示函式 fac> 還有更多部分。

第二部分表示 N 的階乘是 N 乘以 N - 1 的階乘

fac(N) ->
    N * fac(N - 1).

請注意,這部分以 . 結尾,表示此函式沒有其他部分。

編譯檔案

5> c(tut1).
{ok,tut1}

現在計算 4 的階乘。

6> tut1:fac(4).
24

這裡使用引數 4 呼叫模組 tut1 中的函式 fac>

一個函式可以有多個引數。讓我們使用乘以兩個數字的函式來擴充模組 tut1

-module(tut1).
-export([fac/1, mult/2]).

fac(1) ->
    1;
fac(N) ->
    N * fac(N - 1).

mult(X, Y) ->
    X * Y.

請注意,還必須擴充 -export 行,其中包含另一個具有兩個引數的函式 mult 的資訊。

編譯

7> c(tut1).
{ok,tut1}

試用新的函式 mult

8> tut1:mult(3,4).
12

在此範例中,數字是整數,程式碼中函式的引數 NXY 稱為變數。變數必須以大寫字母開頭(請參閱 變數)。變數的範例包括 NumberShoeSizeAge

原子

原子是 Erlang 中的另一種資料類型。原子以小寫字母開頭(請參閱 原子),例如,charlescentimeterinch。原子只是名稱,沒有其他含義。它們不像變數,變數可以有一個值。

將下一個程式輸入到名為 tut2.erl 的檔案中)。它可以讓您方便地在英吋和公分之間進行轉換,反之亦然

-module(tut2).
-export([convert/2]).

convert(M, inch) ->
    M / 2.54;

convert(N, centimeter) ->
    N * 2.54.

編譯

9> c(tut2).
{ok,tut2}

測試

10> tut2:convert(3, inch).
1.1811023622047243
11> tut2:convert(7, centimeter).
17.78

請注意,這裡導入了小數(浮點數),但沒有任何說明。希望您能理解。

讓我們看看如果在 convert 函式中輸入 centimeterinch 以外的內容會發生什麼事

12> tut2:convert(3, miles).
** exception error: no function clause matching tut2:convert(3,miles) (tut2.erl, line 4)

convert 函式的兩個部分稱為其子句。如所示,miles 不屬於任何一個子句。Erlang 系統無法*比對*任何一個子句,因此會傳回錯誤訊息 function_clause。Shell 會適當地格式化錯誤訊息,但錯誤元組會儲存在 Shell 的歷史記錄清單中,可以使用 Shell 命令 v/1 輸出。

13> v(12).
{'EXIT',{function_clause,[{tut2,convert,
                                [3,miles],
                                [{file,"tut2.erl"},{line,4}]},
                          {erl_eval,do_apply,6,
                                    [{file,"erl_eval.erl"},{line,677}]},
                          {shell,exprs,7,[{file,"shell.erl"},{line,687}]},
                          {shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
                          {shell,eval_loop,3,
                                 [{file,"shell.erl"},{line,627}]}]}}

元組

現在 tut2 程式很難說是良好的程式設計風格。請考慮

tut2:convert(3, inch).

這表示 3 的單位是英吋嗎?還是表示 3 的單位是公分,要轉換為英吋?Erlang 提供了一種將事物組合在一起的方法,使事情更容易理解。這些稱為 *元組*,並以大括號 {} 括起來。

因此,{inch,3} 表示 3 英吋,{centimeter,5} 表示 5 公分。現在讓我們編寫一個新的程式,在公分和英吋之間進行轉換,反之亦然。將以下程式碼輸入到名為 tut3.erl 的檔案中)

-module(tut3).
-export([convert_length/1]).

convert_length({centimeter, X}) ->
    {inch, X / 2.54};
convert_length({inch, Y}) ->
    {centimeter, Y * 2.54}.

編譯和測試

14> c(tut3).
{ok,tut3}
15> tut3:convert_length({inch, 5}).
{centimeter,12.7}
16> tut3:convert_length(tut3:convert_length({inch, 5})).
{inch,5.0}

請注意,在第 16 行,5 英吋轉換為公分,然後又轉換回來,並確保恢復到原始值。也就是說,函式的引數可以是另一個函式的結果。請考慮第 16 行(上方)是如何運作的。給予函式 {inch,5} 的引數首先與 convert_length 的第一個標頭子句比對,也就是 convert_length({centimeter,X})。可以看出 {centimeter,X}{inch,5} 不符(標頭是 -> 之前的位元)。由於比對失敗,讓我們嘗試下一個子句的標頭,也就是 convert_length({inch,Y})。這個可以比對成功,Y 的值為 5。

元組可以有多個部分,實際上可以包含任意多個部分,並包含任何有效的 Erlang *術語*。例如,表示世界上各個城市的溫度

{moscow, {c, -10}}
{cape_town, {f, 70}}
{paris, {f, 28}}

元組的項目數量是固定的。元組中的每個項目都稱為 *元素*。在元組 {moscow,{c,-10}} 中,元素 1 是 moscow,元素 2 是 {c,-10}。這裡 c 表示攝氏度,f 表示華氏度。

清單

元組將事物組合在一起,但我們也需要表示事物的清單。Erlang 中的清單以方括號 [] 括起來。例如,世界上各個城市溫度的清單可以是

[{moscow, {c, -10}}, {cape_town, {f, 70}}, {stockholm, {c, -4}},
 {paris, {f, 28}}, {london, {f, 36}}]

請注意,這個列表太長,無法放在同一行。這並不重要,Erlang 允許在所有「合理的位置」換行,但例如,不允許在原子、整數和其他的中間換行。

查看列表部分的一種有用的方法是使用 |。最好用 shell 中的範例來說明:

17> [First |TheRest] = [1,2,3,4,5].
[1,2,3,4,5]
18> First.
1
19> TheRest.
[2,3,4,5]

要將列表的第一個元素與列表的其餘部分分開,可以使用 |First 的值為 1,而 TheRest 的值為 [2,3,4,5]

另一個範例:

20> [E1, E2 | R] = [1,2,3,4,5,6,7].
[1,2,3,4,5,6,7]
21> E1.
1
22> E2.
2
23> R.
[3,4,5,6,7]

在這裡您可以看到使用 | 從列表中取得前兩個元素。如果您嘗試從列表中取得的元素多於列表中的元素,則會傳回錯誤。另請注意沒有元素的列表的特殊情況:[]

24> [A, B | C] = [1, 2].
[1,2]
25> A.
1
26> B.
2
27> C.
[]

在先前的範例中,使用了新的變數名稱,而不是重複使用舊的變數名稱:FirstTheRestE1E2RABC。這樣做的原因是,在變數的上下文中(範圍),變數只能被賦值一次。稍後會詳細說明。

以下範例顯示如何找出列表的長度。請在名為 tut4.erl 的檔案中輸入以下程式碼:

-module(tut4).

-export([list_length/1]).

list_length([]) ->
    0;
list_length([First | Rest]) ->
    1 + list_length(Rest).

編譯和測試

28> c(tut4).
{ok,tut4}
29> tut4:list_length([1,2,3,4,5,6,7]).
7

說明:

list_length([]) ->
    0;

空列表的長度顯然為 0。

list_length([First | Rest]) ->
    1 + list_length(Rest).

第一個元素為 First,其餘元素為 Rest 的列表長度為 1 + Rest 的長度。

(僅限進階讀者:這不是尾遞迴,有更好的方法來編寫此函式。)

一般來說,tuple 用於其他語言中使用「記錄」或「結構」的地方。此外,列表用於表示大小不一的事物,也就是其他語言中使用連結列表的地方。

Erlang 沒有字串資料類型。相反,字串可以用 Unicode 字元的列表表示。例如,這表示列表 [97,98,99] 等同於 "abc"。Erlang shell 很「聰明」,會猜測您的列表是什麼意思,並以它認為最適合的形式輸出,例如:

30> [97,98,99].
"abc"

Map

Map 是一組鍵值關聯。這些關聯使用 #{} 封裝。要建立從 "key" 到值 42 的關聯,請使用:

> #{ "key" => 42 }.
#{"key" => 42}

讓我們直接深入研究一個使用一些有趣功能的範例。

以下範例示範如何使用 map 來參照顏色和 alpha 通道來計算 alpha 混合。請在名為 color.erl 的檔案中輸入程式碼:

-module(color).

-export([new/4, blend/2]).

-define(is_channel(V), (is_float(V) andalso V >= 0.0 andalso V =< 1.0)).

new(R,G,B,A) when ?is_channel(R), ?is_channel(G),
                  ?is_channel(B), ?is_channel(A) ->
    #{red => R, green => G, blue => B, alpha => A}.

blend(Src,Dst) ->
    blend(Src,Dst,alpha(Src,Dst)).

blend(Src,Dst,Alpha) when Alpha > 0.0 ->
    Dst#{
        red   := red(Src,Dst) / Alpha,
        green := green(Src,Dst) / Alpha,
        blue  := blue(Src,Dst) / Alpha,
        alpha := Alpha
    };
blend(_,Dst,_) ->
    Dst#{
        red   := 0.0,
        green := 0.0,
        blue  := 0.0,
        alpha := 0.0
    }.

alpha(#{alpha := SA}, #{alpha := DA}) ->
    SA + DA*(1.0 - SA).

red(#{red := SV, alpha := SA}, #{red := DV, alpha := DA}) ->
    SV*SA + DV*DA*(1.0 - SA).
green(#{green := SV, alpha := SA}, #{green := DV, alpha := DA}) ->
    SV*SA + DV*DA*(1.0 - SA).
blue(#{blue := SV, alpha := SA}, #{blue := DV, alpha := DA}) ->
    SV*SA + DV*DA*(1.0 - SA).

編譯和測試

> c(color).
{ok,color}
> C1 = color:new(0.3,0.4,0.5,1.0).
#{alpha => 1.0,blue => 0.5,green => 0.4,red => 0.3}
> C2 = color:new(1.0,0.8,0.1,0.3).
#{alpha => 0.3,blue => 0.1,green => 0.8,red => 1.0}
> color:blend(C1,C2).
#{alpha => 1.0,blue => 0.5,green => 0.4,red => 0.3}
> color:blend(C2,C1).
#{alpha => 1.0,blue => 0.38,green => 0.52,red => 0.51}

這個範例需要一些說明:

-define(is_channel(V), (is_float(V) andalso V >= 0.0 andalso V =< 1.0)).

首先,定義一個巨集 is_channel 來協助進行 guard 測試。這只是為了方便和減少語法混亂。如需有關巨集的詳細資訊,請參閱 預處理器

new(R,G,B,A) when ?is_channel(R), ?is_channel(G),
                  ?is_channel(B), ?is_channel(A) ->
    #{red => R, green => G, blue => B, alpha => A}.

函式 new/4 會建立一個新的 map term,並讓鍵 redgreenbluealpha 與初始值關聯。在這種情況下,只允許介於 0.0 和 1.0 之間的浮點值(包含 0.0 和 1.0),這是透過每個引數的 ?is_channel/1 巨集來確保的。建立新 map 時,只允許使用 => 運算子。

透過對 new/4 建立的任何顏色 term 呼叫 blend/2,可以根據兩個 map term 計算出產生的顏色。

blend/2 所做的第一件事是計算產生的 alpha 通道:

alpha(#{alpha := SA}, #{alpha := DA}) ->
    SA + DA*(1.0 - SA).

使用 := 運算子取得與鍵 alpha 關聯的值。Map 中的其他鍵會被忽略,只需要檢查鍵 alpha

函式 red/2blue/2green/2 的情況也是如此。

red(#{red := SV, alpha := SA}, #{red := DV, alpha := DA}) ->
    SV*SA + DV*DA*(1.0 - SA).

這裡的不同之處在於會檢查每個 map 引數中的兩個鍵。其他鍵會被忽略。

最後,讓我們在 blend/3 中傳回產生的顏色:

blend(Src,Dst,Alpha) when Alpha > 0.0 ->
    Dst#{
        red   := red(Src,Dst) / Alpha,
        green := green(Src,Dst) / Alpha,
        blue  := blue(Src,Dst) / Alpha,
        alpha := Alpha
    };

Dst map 會更新為新的通道值。更新現有鍵的新值的語法是使用 := 運算子。

標準模組和手冊頁面

Erlang 有許多標準模組來幫助您執行操作。例如,模組 io 包含許多有助於執行格式化輸入/輸出的函式。要查詢有關標準模組的資訊,可以在 erlang shell 中使用指令 h(..)。請嘗試 erlang shell 指令:

1> h(io).

	io

    Standard I/O server interface functions.
    
    This module provides an interface to standard Erlang I/O servers. The output
    functions all return `ok` if they are successful, or exit if they are not.
     ...

如果這在您的系統上無法運作,則文件會以 HTML 格式包含在 Erlang/OTP 版本中。您也可以從 <www.erlang.org/doc> 以 HTML 格式閱讀文件或以 epub 格式下載。

將輸出寫入終端機

能夠在範例中執行格式化輸出是一件好事,因此下一個範例示範使用 io:format/2 函式的簡單方法。與所有其他匯出的函式一樣,您可以在 shell 中測試 io:format/2 函式:

31> io:format("hello world~n", []).
hello world
ok
32> io:format("this outputs one Erlang term: ~w~n", [hello]).
this outputs one Erlang term: hello
ok
33> io:format("this outputs two Erlang terms: ~w~w~n", [hello, world]).
this outputs two Erlang terms: helloworld
ok
34> io:format("this outputs two Erlang terms: ~w ~w~n", [hello, world]).
this outputs two Erlang terms: hello world
ok

函式 io:format/2(也就是說,帶有兩個引數的 format)會接受兩個列表。第一個列表幾乎總是寫在 " " 之間。此列表會按原樣印出,但每個 ~w 會被從第二個列表中依序取得的 term 取代。每個 ~n 會被換行符號取代。如果一切順利,io:format/2 函式本身會傳回原子 ok。與 Erlang 中的其他函式一樣,如果發生錯誤,它會當機。這不是 Erlang 的錯誤,而是一種刻意的策略。Erlang 具有複雜的機制來處理稍後會顯示的錯誤。作為練習,請嘗試讓 io:format/2 當機,這應該不難。但請注意,雖然 io:format/2 當機,Erlang shell 本身不會當機。

較大的範例

現在,來看一個較大的範例,以鞏固您目前所學到的知識。假設您有一個來自世界各地多個城市的溫度讀數列表。其中一些是以攝氏度表示,而另一些則以華氏度表示(如先前的列表)。首先,讓我們將它們全部轉換為攝氏度,然後讓我們整齊地列印資料。

%% This module is in file tut5.erl

-module(tut5).
-export([format_temps/1]).

%% Only this function is exported
format_temps([])->                        % No output for an empty list
    ok;
format_temps([City | Rest]) ->
    print_temp(convert_to_celsius(City)),
    format_temps(Rest).

convert_to_celsius({Name, {c, Temp}}) ->  % No conversion needed
    {Name, {c, Temp}};
convert_to_celsius({Name, {f, Temp}}) ->  % Do the conversion
    {Name, {c, (Temp - 32) * 5 / 9}}.

print_temp({Name, {c, Temp}}) ->
    io:format("~-15w ~w c~n", [Name, Temp]).
35> c(tut5).
{ok,tut5}
36> tut5:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          -10 c
cape_town       21.11111111111111 c
stockholm       -4 c
paris           -2.2222222222222223 c
london          2.2222222222222223 c
ok

在查看這個程式的運作方式之前,請注意程式碼中新增了一些註解。註解以 % 字元開頭,並持續到該行結尾。另請注意,-export([format_temps/1]). 行僅包含函式 format_temps/1。其他函式是區域函式,也就是說,它們在 tut5 模組之外是不可見的。

另請注意,從 shell 測試程式時,輸入會分散在兩行中,因為該行太長。

第一次呼叫 format_temps 時,City 的值會是 {moscow,{c,-10}},而 Rest 是列表的其餘部分。因此,會呼叫函式 print_temp(convert_to_celsius({moscow,{c,-10}}))

以下是以 convert_to_celsius({moscow,{c,-10}}) 作為函式 print_temp 引數的函式呼叫。當函式呼叫像這樣巢狀時,它們會由內而外執行(評估)。也就是說,首先會評估 convert_to_celsius({moscow,{c,-10}}),由於溫度已經是攝氏度,因此會傳回值 {moscow,{c,-10}}。然後,會評估 print_temp({moscow,{c,-10}})。函式 convert_to_celsius 的運作方式與先前範例中的 convert_length 函式類似。

print_temp 只會以與上述類似的方式呼叫 io:format。請注意,~-15w 表示以 15 的欄位長度(寬度)列印「term」並將其靠左對齊。(請參閱 STDLIB 中的 io:fwrite/1 手冊頁面)。

現在會呼叫 format_temps(Rest),並將列表的其餘部分作為引數。這種做法類似於其他語言中的迴圈結構。(是的,這是遞迴,但不要讓這困擾您。)因此,會再次呼叫相同的 format_temps 函式,這次 City 的值會是 {cape_town,{f,70}},並重複先前的相同程序。這會一直執行,直到列表變成空白,也就是 [],這會導致第一個子句 format_temps([]) 符合。這只會傳回(產生)原子 ok,因此程式結束。

比對、Guard 和變數範圍

在這樣的列表中找出最大和最小溫度可能很有用。在擴充程式來執行此操作之前,讓我們看看用於找出列表中元素最大值的函式:

-module(tut6).
-export([list_max/1]).

list_max([Head|Rest]) ->
   list_max(Rest, Head).

list_max([], Res) ->
    Res;
list_max([Head|Rest], Result_so_far) when Head > Result_so_far ->
    list_max(Rest, Head);
list_max([Head|Rest], Result_so_far)  ->
    list_max(Rest, Result_so_far).
37> c(tut6).
{ok,tut6}
38> tut6:list_max([1,2,3,4,5,7,4,3,2,1]).
7

首先請注意,有兩個函式具有相同的名稱 list_max。但是,每個函式接受不同數量的引數(參數)。在 Erlang 中,這些會被視為完全不同的函式。如果您需要區分這些函式,請寫入 Name/Arity,其中 Name 是函式名稱,而 Arity 是引數的數量,在此案例中為 list_max/1list_max/2

在這個範例中,您會「攜帶」一個值(在此案例中為 Result_so_far)走訪列表。list_max/1 只會假設列表的最大值是列表的頭,並使用列表的其餘部分和列表頭的值呼叫 list_max/2。在上述範例中,這會是 list_max([2,3,4,5,7,4,3,2,1],1)。如果您嘗試使用空列表使用 list_max/1,或嘗試使用根本不是列表的東西,就會導致錯誤。請注意,Erlang 的哲學不是在錯誤發生的函式中處理此類型的錯誤,而是在其他地方執行此操作。稍後會詳細說明。

list_max/2 中,您會遍歷列表,當 Head > Result_so_far 時,使用 Head 取代 Result_so_farwhen 是一個特殊的關鍵字,在函數中的 -> 之前使用,表示只有在後面的測試為真時才使用函數的此部分。這種測試稱為守衛。如果守衛為假(也就是說,守衛失敗),則會嘗試函數的下一部分。在這種情況下,如果 Head 不大於 Result_so_far,那麼它必須小於或等於它。這表示函數的下一部分不需要守衛。

守衛中一些有用的運算符如下:

  • < 小於
  • > 大於
  • == 等於
  • >= 大於或等於
  • =< 小於或等於
  • /= 不等於

(請參閱守衛序列)。

若要將上述程式更改為計算列表中元素的最小值,您只需要寫 < 取代 >。(但將函數名稱更改為 list_min 會是明智之舉。)

前面提到變數在其作用域中只能賦值一次。在上面的程式碼中,您會看到 Result_so_far 被賦予多個值。這是可以的,因為每次呼叫 list_max/2 時,都會建立一個新的作用域,而且可以將 Result_so_far 視為每個作用域中的不同變數。

另一種建立變數並賦值的方法是使用匹配運算符 =。因此,如果您寫 M = 5,就會建立一個名為 M 的變數,其值為 5。如果在同一作用域中,您接著寫 M = 6,則會傳回錯誤。在 shell 中試試看

39> M = 5.
5
40> M = 6.
** exception error: no match of right hand side value 6
41> M = M + 1.
** exception error: no match of right hand side value 6
42> N = M + 1.
6

匹配運算符的用途在於分解 Erlang 術語並建立新的術語。

43> {X, Y} = {paris, {f, 28}}.
{paris,{f,28}}
44> X.
paris
45> Y.
{f,28}

這裡 X 的值為 paris,而 Y 的值為 {f,28}

如果您嘗試使用另一個城市再次執行相同的操作,則會傳回錯誤

46> {X, Y} = {london, {f, 36}}.
** exception error: no match of right hand side value {london,{f,36}}

變數還可用於提高程式碼的可讀性。例如,在上面的函數 list_max/2 中,您可以寫:

list_max([Head|Rest], Result_so_far) when Head > Result_so_far ->
    New_result_far = Head,
    list_max(Rest, New_result_far);

這可能更清楚一些。

關於列表的更多資訊

請記住,| 運算符可用於取得列表的頭部

47> [M1|T1] = [paris, london, rome].
[paris,london,rome]
48> M1.
paris
49> T1.
[london,rome]

| 運算符也可用於將頭部新增至列表

50> L1 = [madrid | T1].
[madrid,london,rome]
51> L1.
[madrid,london,rome]

現在示範在處理列表時使用它的範例 - 反轉列表的順序

-module(tut8).

-export([reverse/1]).

reverse(List) ->
    reverse(List, []).

reverse([Head | Rest], Reversed_List) ->
    reverse(Rest, [Head | Reversed_List]);
reverse([], Reversed_List) ->
    Reversed_List.
52> c(tut8).
{ok,tut8}
53> tut8:reverse([1,2,3]).
[3,2,1]

請考慮如何建立 Reversed_List。它首先為 [],然後連續從要反轉的列表中取出頭部,並將其新增至 Reversed_List,如下所示

reverse([1|2,3], []) =>
    reverse([2,3], [1|[]])

reverse([2|3], [1]) =>
    reverse([3], [2|[1])

reverse([3|[]], [2,1]) =>
    reverse([], [3|[2,1]])

reverse([], [3,2,1]) =>
    [3,2,1]

lists 模組包含許多操作列表的函數,例如,反轉列表。因此,在編寫操作列表的函數之前,最好先檢查是否有已經為您寫好的函數(請參閱 STDLIB 中的 lists 手冊頁)。

現在讓我們回到城市和溫度,但這次採取更有結構的方法。首先,讓我們將整個列表轉換為攝氏度,如下所示

-module(tut7).
-export([format_temps/1]).

format_temps(List_of_cities) ->
    convert_list_to_c(List_of_cities).

convert_list_to_c([{Name, {f, F}} | Rest]) ->
    Converted_City = {Name, {c, (F -32)* 5 / 9}},
    [Converted_City | convert_list_to_c(Rest)];

convert_list_to_c([City | Rest]) ->
    [City | convert_list_to_c(Rest)];

convert_list_to_c([]) ->
    [].

測試函數

54> c(tut7).
{ok, tut7}.
55> tut7:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
[{moscow,{c,-10}},
 {cape_town,{c,21.11111111111111}},
 {stockholm,{c,-4}},
 {paris,{c,-2.2222222222222223}},
 {london,{c,2.2222222222222223}}]

說明:

format_temps(List_of_cities) ->
    convert_list_to_c(List_of_cities).

這裡 format_temps/1 呼叫 convert_list_to_c/1convert_list_to_c/1 會取出 List_of_cities 的頭部,並在需要時將其轉換為攝氏度。| 運算符用於將(可能)已轉換的頭部新增至列表的其餘部分(已轉換)

[Converted_City | convert_list_to_c(Rest)];

[City | convert_list_to_c(Rest)];

這樣做直到到達列表的末尾,也就是說,列表為空

convert_list_to_c([]) ->
    [].

現在,當列表轉換完成時,會新增一個函數來列印它

-module(tut7).
-export([format_temps/1]).

format_temps(List_of_cities) ->
    Converted_List = convert_list_to_c(List_of_cities),
    print_temp(Converted_List).

convert_list_to_c([{Name, {f, F}} | Rest]) ->
    Converted_City = {Name, {c, (F -32)* 5 / 9}},
    [Converted_City | convert_list_to_c(Rest)];

convert_list_to_c([City | Rest]) ->
    [City | convert_list_to_c(Rest)];

convert_list_to_c([]) ->
    [].

print_temp([{Name, {c, Temp}} | Rest]) ->
    io:format("~-15w ~w c~n", [Name, Temp]),
    print_temp(Rest);
print_temp([]) ->
    ok.
56> c(tut7).
{ok,tut7}
57> tut7:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          -10 c
cape_town       21.11111111111111 c
stockholm       -4 c
paris           -2.2222222222222223 c
london          2.2222222222222223 c
ok

現在必須新增一個函數來找出具有最高和最低溫度的城市。下列程式並不是執行此操作最有效的方法,因為您會遍歷城市列表四次。但是,最好先力求清晰和正確,只有在需要時才使程式高效。

-module(tut7).
-export([format_temps/1]).

format_temps(List_of_cities) ->
    Converted_List = convert_list_to_c(List_of_cities),
    print_temp(Converted_List),
    {Max_city, Min_city} = find_max_and_min(Converted_List),
    print_max_and_min(Max_city, Min_city).

convert_list_to_c([{Name, {f, Temp}} | Rest]) ->
    Converted_City = {Name, {c, (Temp -32)* 5 / 9}},
    [Converted_City | convert_list_to_c(Rest)];

convert_list_to_c([City | Rest]) ->
    [City | convert_list_to_c(Rest)];

convert_list_to_c([]) ->
    [].

print_temp([{Name, {c, Temp}} | Rest]) ->
    io:format("~-15w ~w c~n", [Name, Temp]),
    print_temp(Rest);
print_temp([]) ->
    ok.

find_max_and_min([City | Rest]) ->
    find_max_and_min(Rest, City, City).

find_max_and_min([{Name, {c, Temp}} | Rest],
         {Max_Name, {c, Max_Temp}},
         {Min_Name, {c, Min_Temp}}) ->
    if
        Temp > Max_Temp ->
            Max_City = {Name, {c, Temp}};           % Change
        true ->
            Max_City = {Max_Name, {c, Max_Temp}} % Unchanged
    end,
    if
         Temp < Min_Temp ->
            Min_City = {Name, {c, Temp}};           % Change
        true ->
            Min_City = {Min_Name, {c, Min_Temp}} % Unchanged
    end,
    find_max_and_min(Rest, Max_City, Min_City);

find_max_and_min([], Max_City, Min_City) ->
    {Max_City, Min_City}.

print_max_and_min({Max_name, {c, Max_temp}}, {Min_name, {c, Min_temp}}) ->
    io:format("Max temperature was ~w c in ~w~n", [Max_temp, Max_name]),
    io:format("Min temperature was ~w c in ~w~n", [Min_temp, Min_name]).
58> c(tut7).
{ok, tut7}
59> tut7:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          -10 c
cape_town       21.11111111111111 c
stockholm       -4 c
paris           -2.2222222222222223 c
london          2.2222222222222223 c
Max temperature was 21.11111111111111 c in cape_town
Min temperature was -10 c in moscow
ok

If 和 Case

函數 find_max_and_min 會計算最高和最低溫度。這裡引入了一個新的結構,if。If 的運作方式如下

if
    Condition 1 ->
        Action 1;
    Condition 2 ->
        Action 2;
    Condition 3 ->
        Action 3;
    Condition 4 ->
        Action 4
end

請注意,在 end 之前沒有 ;。條件與守衛的作用相同,也就是說,測試會成功或失敗。Erlang 從頂部開始測試,直到找到成功的條件。然後,它會評估(執行)條件後面的動作,並忽略 end 之前的所有其他條件和動作。如果沒有符合的條件,就會發生執行階段失敗。始終成功的條件是原子 true。這通常在 if 中最後使用,表示如果所有其他條件都失敗,則執行 true 後面的動作。

以下是一個簡短的程式,用於示範 if 的運作方式。

-module(tut9).
-export([test_if/2]).

test_if(A, B) ->
    if
        A == 5 ->
            io:format("A == 5~n", []),
            a_equals_5;
        B == 6 ->
            io:format("B == 6~n", []),
            b_equals_6;
        A == 2, B == 3 ->                      %That is A equals 2 and B equals 3
            io:format("A == 2, B == 3~n", []),
            a_equals_2_b_equals_3;
        A == 1 ; B == 7 ->                     %That is A equals 1 or B equals 7
            io:format("A == 1 ; B == 7~n", []),
            a_equals_1_or_b_equals_7
    end.

測試此程式碼會產生

60> c(tut9).
{ok,tut9}
61> tut9:test_if(5,33).
A == 5
a_equals_5
62> tut9:test_if(33,6).
B == 6
b_equals_6
63> tut9:test_if(2, 3).
A == 2, B == 3
a_equals_2_b_equals_3
64> tut9:test_if(1, 33).
A == 1 ; B == 7
a_equals_1_or_b_equals_7
65> tut9:test_if(33, 7).
A == 1 ; B == 7
a_equals_1_or_b_equals_7
66> tut9:test_if(33, 33).
** exception error: no true branch found when evaluating an if expression
     in function  tut9:test_if/2 (tut9.erl, line 5)

請注意,tut9:test_if(33,33) 不會導致任何條件成功。這會導致執行階段錯誤 if_clause,此處由 shell 進行良好格式化。有關可用的許多守衛測試的詳細資訊,請參閱守衛序列

case 是 Erlang 中的另一個結構。回想一下,convert_length 函數的寫法如下:

convert_length({centimeter, X}) ->
    {inch, X / 2.54};
convert_length({inch, Y}) ->
    {centimeter, Y * 2.54}.

相同的程式碼也可以寫成

-module(tut10).
-export([convert_length/1]).

convert_length(Length) ->
    case Length of
        {centimeter, X} ->
            {inch, X / 2.54};
        {inch, Y} ->
            {centimeter, Y * 2.54}
    end.
67> c(tut10).
{ok,tut10}
68> tut10:convert_length({inch, 6}).
{centimeter,15.24}
69> tut10:convert_length({centimeter, 2.5}).
{inch,0.984251968503937}

caseif 都有傳回值,也就是說,在上面的範例中,case 傳回 {inch,X/2.54}{centimeter,Y*2.54}case 的行為也可以透過使用守衛來修改。下列範例闡明了這一點。它會告訴我們給定年份的月份長度。年份必須已知,因為閏年的二月有 29 天。

-module(tut11).
-export([month_length/2]).

month_length(Year, Month) ->
    %% All years divisible by 400 are leap
    %% Years divisible by 100 are not leap (except the 400 rule above)
    %% Years divisible by 4 are leap (except the 100 rule above)
    Leap = if
        trunc(Year / 400) * 400 == Year ->
            leap;
        trunc(Year / 100) * 100 == Year ->
            not_leap;
        trunc(Year / 4) * 4 == Year ->
            leap;
        true ->
            not_leap
    end,
    case Month of
        sep -> 30;
        apr -> 30;
        jun -> 30;
        nov -> 30;
        feb when Leap == leap -> 29;
        feb -> 28;
        jan -> 31;
        mar -> 31;
        may -> 31;
        jul -> 31;
        aug -> 31;
        oct -> 31;
        dec -> 31
    end.
70> c(tut11).
{ok,tut11}
71> tut11:month_length(2004, feb).
29
72> tut11:month_length(2003, feb).
28
73> tut11:month_length(1947, aug).
31

內建函數 (BIF)

BIF 是由於某些原因內建於 Erlang 虛擬機器中的函數。BIF 通常實作無法在 Erlang 中實作或實作效率太低的功能。某些 BIF 可以僅使用函數名稱呼叫,但它們預設屬於 erlang 模組。例如,呼叫 BIF trunc 如下所示,相當於呼叫 erlang:trunc

如所示,首先會檢查年份是否為閏年。如果年份可以被 400 整除,則為閏年。若要判斷這一點,請先將年份除以 400,並使用 BIF trunc(稍後會詳細介紹)來截斷任何小數。然後再乘以 400,看看是否傳回相同的值。例如,西元 2004 年

2004 / 400 = 5.01
trunc(5.01) = 5
5 * 400 = 2000

2000 與 2004 不同,因此 2004 不能被 400 整除。西元 2000 年

2000 / 400 = 5.0
trunc(5.0) = 5
5 * 400 = 2000

也就是說,為閏年。接下來的兩個 trunc 測試會以相同的方式評估年份是否可以被 100 或 4 整除。第一個 if 會傳回 leapnot_leap,並進入變數 Leap。此變數會用於下列 casefeb 的守衛,告訴我們月份的長度。

此範例示範了 trunc 的用法。使用 Erlang 運算符 rem 會更容易,該運算符會傳回除法後的餘數,例如

74> 2004 rem 400.
4

因此,與其寫成

trunc(Year / 400) * 400 == Year ->
    leap;

不如寫成

Year rem 400 == 0 ->
    leap;

還有許多其他 BIF,例如 trunc。只有少數 BIF 可以在守衛中使用,而且您不能在守衛中使用自己定義的函數。(請參閱守衛序列)(針對進階讀者:這是為了確保守衛沒有副作用。)讓我們在 shell 中試用其中一些函數

75> trunc(5.6).
5
76> round(5.6).
6
77> length([a,b,c,d]).
4
78> float(5).
5.0
79> is_atom(hello).
true
80> is_atom("hello").
false
81> is_tuple({paris, {c, 30}}).
true
82> is_tuple([paris, {c, 30}]).
false

所有這些都可以在守衛中使用。現在來看一些不能在守衛中使用的 BIF

83> atom_to_list(hello).
"hello"
84> list_to_atom("goodbye").
goodbye
85> integer_to_list(22).
"22"

這三個 BIF 進行的轉換在 Erlang 中很難(或不可能)執行。

高階函數 (Fun)

Erlang 與大多數現代函數式程式設計語言一樣,都具有高階函數。以下是在 shell 中使用的範例

86> Xf = fun(X) -> X * 2 end.
#Fun<erl_eval.5.123085357>
87> Xf(5).
10

這裡定義了一個函數,將數字的值加倍,並將此函數指派給變數。因此 Xf(5) 會傳回值 10。處理列表時,兩個有用的函數是 foreachmap,其定義如下

foreach(Fun, [First|Rest]) ->
    Fun(First),
    foreach(Fun, Rest);
foreach(Fun, []) ->
    ok.

map(Fun, [First|Rest]) ->
    [Fun(First)|map(Fun,Rest)];
map(Fun, []) ->
    [].

這兩個函數在標準模組 lists 中提供。foreach 會採用一個列表,並將 fun 套用至列表中的每個元素。map 會藉由將 fun 套用至列表中的每個元素來建立新列表。回到 shell,會使用 map 和 fun 來將 3 新增至列表的每個元素

88> Add_3 = fun(X) -> X + 3 end.
#Fun<erl_eval.5.123085357>
89> lists:map(Add_3, [1,2,3]).
[4,5,6]

讓我們(再次)列印城市列表中的溫度

90> Print_City = fun({City, {X, Temp}}) -> io:format("~-15w ~w ~w~n",
[City, X, Temp]) end.
#Fun<erl_eval.5.123085357>
91> lists:foreach(Print_City, [{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          c -10
cape_town       f 70
stockholm       c -4
paris           f 28
london          f 36
ok

現在讓我們定義一個 fun,可用於遍歷城市和溫度的列表,並將它們全部轉換為攝氏度。

-module(tut13).

-export([convert_list_to_c/1]).

convert_to_c({Name, {f, Temp}}) ->
    {Name, {c, trunc((Temp - 32) * 5 / 9)}};
convert_to_c({Name, {c, Temp}}) ->
    {Name, {c, Temp}}.

convert_list_to_c(List) ->
    lists:map(fun convert_to_c/1, List).
92> tut13:convert_list_to_c([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
[{moscow,{c,-10}},
 {cape_town,{c,21}},
 {stockholm,{c,-4}},
 {paris,{c,-2}},
 {london,{c,2}}]

convert_to_c 函數與之前相同,但這裡將其用作 fun

lists:map(fun convert_to_c/1, List)

當在其他地方定義的函數用作 fun 時,可以將其稱為 Function/Arity(請記住,Arity = 引數的數量)。因此,在 map 呼叫中,會寫成 lists:map(fun convert_to_c/1, List)。如所示,convert_list_to_c 變得更短且更容易理解。

標準模組 lists 也包含函數 sort(Fun, List),其中 Fun 是具有兩個引數的 fun。如果第一個引數小於第二個引數,則此 fun 會傳回 true,否則傳回 false。排序會新增至 convert_list_to_c

-module(tut13).

-export([convert_list_to_c/1]).

convert_to_c({Name, {f, Temp}}) ->
    {Name, {c, trunc((Temp - 32) * 5 / 9)}};
convert_to_c({Name, {c, Temp}}) ->
    {Name, {c, Temp}}.

convert_list_to_c(List) ->
    New_list = lists:map(fun convert_to_c/1, List),
    lists:sort(fun({_, {c, Temp1}}, {_, {c, Temp2}}) ->
                       Temp1 < Temp2 end, New_list).
93> c(tut13).
{ok,tut13}
94> tut13:convert_list_to_c([{moscow, {c, -10}}, {cape_town, {f, 70}},
{stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
[{moscow,{c,-10}},
 {stockholm,{c,-4}},
 {paris,{c,-2}},
 {london,{c,2}},
 {cape_town,{c,21}}]

sort 中,會使用 fun

fun({_, {c, Temp1}}, {_, {c, Temp2}}) -> Temp1 < Temp2 end,

這裡引入了匿名變數 _ 的概念。這只是取得值但會忽略該值的變數的簡寫。這可以在任何適當的地方使用,而不僅僅是在 fun 中。Temp1 < Temp2 會在 Temp1 小於 Temp2 時傳回 true