Erlang程式設計與問題解決/Erlang/OTP 程式庫

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

本章介紹 Erlang 開源電信平台 (Open Telecom Platform, OTP) 的執行環境指令與程式庫。

Erlang/OTP 平台提供一個編譯器、一個直譯器、一套程式庫、一套伺服器協定、以及 CORBA 。 Erlang/OTP 的 Windows 版本平台有視窗模式的控制台可用。 Erlang 是函數語言,在 Erlang 控制台中,使用指令也是函數呼叫。當你啟動控制台時,會載入 Erlang 程式庫。 Erlang 程式庫區分為許多模組,負責處理多種方面的問題與需求。 Erlang 控制台中,內定的模組為 erlang ,當執行 erlang 模組的函數時,可以不加上模組前綴字 ( erlang: ) 。使用其他模組則必須以模組名稱做為前綴字。每一個模組提供許多函數,這些函數都為了執行與該模組有關的操作。

我們稱各種模組提供的各種函數為:內建函數 ( Built-In Function, BIF ) 。

模組簡介[编辑]

Erlang 分為幾個應用程式,有 kernel 、 compiler 、 stdlib 、 mnesia ... 等等。

應用程式概觀[编辑]

當你要使用 Erlang ,是以使用 Erlang runtime 系統為開始。 Erlang 以 kernel 應用程式開始,載入一些控制器、編碼服務、錯誤記錄服務、作業系統及網路服務模組等等。由 sasl 應用程式 ( 系統程式支援 ) 提供警訊、回報系統及系統工具。

當你在撰寫 Erlang 程式時, stdlib 程式庫提供了大量內建函數,提供一些應用方面的細節,包含資料、行程、檔案、網路等各方面的操作功能。 mnesia 應用程式提供分散式的資料庫管理系統,以關聯式及物件式混搭模型做資料操作。

STDLIB 模組列表[编辑]

資料結構方面
有 array 模組、 lists 模組、 string 模組、 sets 模組、 ordsets 模組 ( ordered set ) 、 sofs 模組 ( set of sets ) 、 queue 模組 、 dict 模組、 orddict 模組 ( ordered dictionary ) 、 proplists 模組 ( property lists ) 、 digraph 模組及 digraph_utils 模組、 gb_trees 模組 ( general balanced trees ) 與 gb_sets ( 用 general balanced tree 做 ordered set ) 。
行程方面
有 proc_lib 模組 ( 提供行程操作 ) 、 pool 模組 ( 管理 Erlang 行程 ) 。
資料庫方面
qlc 模組 ( 提供對 ets 、 dets 與 mnesia 資料表查詢的介面語言 ) 。
網路方面
pg 模組 ( distributed, named process groups ) 、 slave 模組 ( 提供網路子節點操作 ) 。
樣版
有一些模組提供應用程式樣版,有 gen_server 模組、 gen_fsm 模組 ( finite state machine ) 、 gen_event 模組、 supervisor 模組、 supervisor_bridge 模組
io 與 io_lib 模組
提供輸入或輸出函數。
calendar 模組
處理日期格式。
ets 模組
稱為 Erlang Term Service ,提供對大量資料做隨機存取的功能,在記憶體中處理資料。
dets 模組
ets 的磁碟版本,在檔案中處理資料。
filelib 模組
檔案系統工具。
file_sorter 模組
提供將檔案內容排序或合併的功能。
filename 模組
處理檔案名稱的擷取、分割或合併。
zip 模組
操作 zip 檔案。
math 模組
提供常用的數學公式演算。
random 模組:提供隨機數操作。
regexp 及 re 模組
對字串做正規表達檢查。
timer 模組
提供計時操作。
unicode 模組
提供 Unicode 字碼轉換。
sys 模組
提供系統資訊操作,包含統計程式執行結果、追蹤程式執行 ... 等等。

Erlang 控制台模組[编辑]

當進入 Erlang 控制台時,會預先載入一些模組,其中有 c 模組,稱為命令介面模組 (Command Interface Module) 。在 Erlang 控制台使用以下命令時,不需要前綴模組名稱。

ls/0, ls/1
類似 Unix/Linux 的檔案列表命令 ls ,也類似 Windows 的 dir

ls/0 和 ls/1 都是沒有傳回值的函數,只有執行時會做出列出目錄檔案列表的效果。 ls/0 列出目前目錄檔案,而 ls/1 列出指定目錄的檔案。目錄名稱為字串。

cd/1
更換目錄,用法與 Window 、 Unix/Linux 的 cd 相同。

cd/1 需要一個參數,為絕對檔案路徑或相對檔案路徑。 cd/1 函數沒有傳回值,只有執行切換目錄的效果。切換之後,會印出當前目錄的路徑。如果切換之後印出的目錄路徑不變,表示目錄沒有完成切換,隱含了所指定要切換的絕對檔案路徑或相對檔案路徑無效。

c/1, c/2
編譯 Erlang 程式。

c/1 和 c/2 是用來將 .erl 編譯為 .beam 檔案,以供程式執行。 c/1 直接編譯目前目錄能看到的模組,需要給它一個參數,為模組名稱:例如,檔名為 "test.erl" ,編譯命令則是 "c(test)." 。當編譯成功時,傳回 {ok,模組名稱} ;失敗則傳回 error 。程式如果需要警告或警示錯誤,會在編譯時一併印出相關訊息。

> c(test).
{ok,test}
> c(empty).
./empty.erl:none: no such file or directory
error

c/2 等同於 compile:file/2 ,需要一個模組名稱與編譯參數。

halt/0, q/0
關閉 Erlang 控制台。

halt/0 是將 Erlang runtime 系統正常關閉。 q/0 是呼叫 init:stop/0 ,以在控制之內的方式使 Erlang 程序結束。

halt/0 是 erlang 模組內建函數 ( erlang:halt/0 ) 。有一些 erlang 模組的函數,使用時不必在前面加綴模組名稱。

help/0
列出命令列表。

Erlang 控制台還提供許多便利的命令,可以使用 help(). 命令,列出清單及說明。

lists 模組[编辑]

前面幾章大量使用了 lists:seq/2 。當我需要用到一串數字,我不自己打字輸入,而是叫 Erlang 幫忙產生的時候,就會用到 lists:seq/2 。 lists:seq/2 的定義是:參數給二個數字,它會產生從第一個數字到第二數字的遞增序列。

列表的產生[编辑]

> lists:seq(1,100).
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,
 23,24,25,26,27,28,29|...]

有時,我想要為一個列表的每個項目都加上一個編號,這時會使用 lists:zip/2 ,把這個列表跟一個對等的整數列表兩兩貼齊,像拉上拉鏈一樣。 lists:zip/2 的定義是:將二個長度相同的列表「拉上拉鏈」連接起來。以下例子,是將列表加上索引,使結果看起來像陣列。

-module(example).
-export([indexing/1]).
indexing(Xs) ->
    Is = lists:seq(0,length(Xs)-1),
    lists:zip(Is,Xs).
> example:indexing([a,b,c]).
[{0,a},{1,b},{2,c}]

lists:map/2 容許你給一個函數,讓列表的每一個項目都套用這個函數:例如,以下是將列表的每個項目都求平方。

> lists:map(fun (X) -> X * X end, lists:seq(1,10)).
[1,4,9,16,25,36,49,64,81,100]

列表的增刪[编辑]

有二個函數與 lists:map/2 很類似, lists:takewhile/2 是依序判斷列表每一個項目,如果判斷函數傳回 true ,就從列表取出該項目,如此一直取到遇到不符合判斷函數為止。, lists:dropwhil/2 相對地,是依序判斷列表每一個項目,如果判斷函數傳回 true 就從列表刪掉該項目,否則就中斷,傳回剩下的列表。也就是,前者從列表前段取最長一段完全符合判斷函數的子列表,後者從列表前段刪去最長一段完全符合判斷函數的子列表。以下,示範二個例子,分別展示二個函數。

> lists:takewhile(fun (X) -> X > 5 end, [6,6,8,7,4,8]).
[6,6,8,7]
> lists:dropwhile(fun (X) -> X > 5 end, [6,6,5,7,4,8]).
[5,7,4,8]

lists:filter/2 與前二者有相同的操作方式。 lists:filter/2 的定義是:根據指定的判斷函數,將列表中符合條件的項目取出。

> lists:filter(fun(X)-> X /= nil end, [nil,{a,b,d},{b,nil,c},{d,nil,nil}]).
[{a,b,d},{b,nil,c},{d,nil,nil}]

而這樣的函數呼叫,與以下的列表列示 ( list comprehension ) 有相同的意義:

> [ X || X <- [nil,{a,b,d},{b,nil,c},{d,nil,nil}], X /= nil ].
[{a,b,d},{b,nil,c},{d,nil,nil}]

列表的更換順序[编辑]

lists:reverse/1 可以把列表前後順序倒轉。 lists:sort/1 可以求排序之後的列表。

如果你有個列表,其中每一項目是以值組 ( tuple ) 表達,這個列表的排序是要用值組的其中一個項目做關鍵值排序。對於這樣的需求, lists:keysort/2 可以提供服務:例如,對 [{8,c},{4,a},{6,b},{5,d}] 分別用每個項目的第一欄排序,或是用第二欄排序,結果為

> lists:keysort(1,[{8,c},{4,a},{6,b},{5,d}]).
[{4,a},{5,d},{6,b},{8,c}]
> lists:keysort(2,[{8,c},{4,a},{6,b},{5,d}]).
[{4,a},{6,b},{8,c},{5,d}]

另外,有 lists:merge/2 函數可將二個已排序的列表做合併。

> lists:merge([1,3,5],[2,4,6]).
[1,2,3,4,5,6]
> lists:merge([2,4,6],[1,3,5]).
[1,2,3,4,5,6]
> lists:merge([4,5,6],[1,2,3]).
[1,2,3,4,5,6]

列表擷取、分割、合併[编辑]

子列表的擷取,除了可以用 lists:takewhile/2 、 lists:dropwhile/2 之外,還可以用 lists:sublist/2lists:sublist/3 ,直接指定要擷取的長度與位置。 lists:sublist/2 給一個長度參數,會取出指定長度的子列表。

> lists:sublist([1,2,3,4], 3).
[1,2,3]
> lists:sublist([1,2,3,4], 5).
[1,2,3,4]

lists:sublist/3 給一個開始位置參數和一個長度參數,會從指定位置開始,取指定長度的子列表。

> lists:sublist([1,2,3,4],1,2).
[1,2]
> lists:sublist([1,2,3,4],2,4).
[2,3,4]

須注意,用在列表的項目位置是以 1 為底,不像一般談陣列元素位置都是以 0 為底。以上 lists:sublist/2 、 lists:sublist/3 的位置和長度參數都要用大於 0 的數字。

lists:split/2 可用指定的長度參數,將列表分割為前後二段,並且前段長度為指定的長度。長度參數有邊界限制。

> lists:split(0, [a,b,c]).
{[],[a,b,c]}

另外,有 lists:partition/2 可以根據一個判斷函數,將列表分為符合的部份與不符合的部份。

> lists:partition(fun (X) -> X < 5 end, [1,5,6,2,3,7,8,4]).
{[1,2,3,4],[5,6,7,8]}

有時,我們會拿到巢狀的列表,列表的每個元素仍是列表。如果需要將這些深度的列表結構攤平,可以使用 lists:flatten/1

> lists:flatten([[[]],[],[[]]]).
[]
> lists:flatten([[[a,b],[c,d]],[[e,f] ]]).
[a,b,c,d,e,f]

對於上例的情況, [[[a,b],[c,d]],[[e,f] ]] ,有時會想要把 [a,b] 、 [c,d] 、 [e,f] 等單元結構保留。這種情況恰好可以使用 lists:append/1

> lists:append([[[a,b],[c,d]],[[e,f] ]]).
[[a,b],[c,d],[e,f]]

另外有個 lists:concat/1 函數,與 lists:append/1 很像,不過, lists:concat/1 是將列表中的每一項目都當作字串,並將原子項目轉換為字串,結果是傳回一個合併字串。

> lists:concat([a,b,c,d,e,f]).
"abcdef"

不過在某方面, lists:concat/1 與 lists:append/1 擁有相同效果。

> lists:concat([[[a,b],[c,d]],[[e,f] ]]).
[[a,b],[c,d],[e,f]]

列表元素操作[编辑]

列表的首元素可以由以 [H|_] 樣式匹配而取出 H 值。最後一個列表元素則用 lists:last/1 取得。

至於任何一個位置的列表元素,則使用 lists:nth/2 取出。

> lists:nth(2, [a,b,c,d]).
b

使用 lists:delete/2 可以將指定的元素,從列表中第一個符合的位置,把元素刪除。

> lists:delete(1,[1,1,1,1]).
[1,1,1]

效果與使用 -- [1] 相同。

> [1,1,1,1] -- [1].
[1,1,1]

另外有 lists:subtract/2 函數與 -- 運算子等價。

高階列表函數[编辑]

lists:foldr/3lists:foldl/3 提供以摺疊方式處理一列資料。摺疊的概念是說,每一摺都是將新的一摺摺到已經摺好的摺疊組上。重寫一次,每一摺都是將「新的一摺」「摺到」「已經摺好的摺疊組」上。「新的一摺」、「摺到」、「已經摺好的摺疊組」,只要抓住這些元素,再加上「空的摺疊組是 [] 」這樣的基礎,就可以用一個函數把這種摺疊的過程表達出來。首先,「摺到」是一個函數,它有二個參數分別是「新的一摺」和「已經摺好的摺疊組」。以下例子是在 Erlang 控制台,用 lambda 表示法定義一個「摺到」函數。

> Fold = fun (X, Y) -> [X|Y] end.

lists:foldr/3lists:foldl/3 接受相同的三個參數,分別是,摺疊函數、基本情況與要處理的列表。當我們想要從一個列表建立出另一個相同的列表時,摺疊函數是上述的 Fold ,基本情況是 [] 。於是,以下例子展示重複一個列表自身而產生新列表的函數呼叫。

> lists:foldr(Fold, [], [a,b,c]).
[a,b,c]

如果我們寫摺疊函數如下:

> Plus = fun (X, Y) -> X + Y end.

那麼,以下程式實現 lists:sum/1 ,將一個列表值加總。

> lists:foldr(Plus, 0, lists:seq(1,10)).
55

練習[编辑]

一元二次數學式
請寫一函數,將係數───依序為 [1,2,1] ───表示為一元二次數學式,即 X2 + 2X1 + X0 。當函數接受了一個數代表 X ,就會計算結果。
一元五次數學式
請寫一函數,用到 lists:foldl/3 而不用遞迴,將係數───依序為 [3,0,5,2,4,1] ───表示為一元五次數學式,即 3X5 + 0X4 + 5X3 + 2X2 + 4X1 + X0 。當函數接受了一個數代表 X ,就會計算結果。

陣列模組[编辑]

Erlang/OTP標準程式庫的陣列模組 array ,用來製作函數式、並且可延長的陣列。稱為陣列,特色當然是以 0 為底。 ( 再次強調,陣列以 0 為底,而相較地,列表和字串以及值組都以 1 為底。 ) 陣列長度可以固定,也可以自動延長。陣列可以在產生時賦值,否則,陣列的每個元素以 undefined 為預設值。使用 array:reset/2 可以將陣列內容回復為預設值。使用 array:resize/2array:resize/1 可以調整陣列的長度。須注意,陣列長度不會自動縮短,一但你用了索引值 i 在重新調整陣列長度之前,從索引值 0 ... i 的陣列空間會一直留著。

陣列的產生[编辑]

使用 array:new/1 建立新陣列,要加上一些參數,這些參數是分別是一些表示陣列長度、固定性及預設值的詞彙。詞彙表達為:

  1. 任何數字,或 {size, 數字} 代表陣列長度。
  2. fixed 或 {fixed, true} 代表固定陣列; {fixed, false} 代表變動陣列。
  3. {default, 值} 代表預設值。

打開 Erlang 主控台,輸入以下情況並觀察結果:

 ( 建立一個陣列,擁有一個元素。陣列固定長度,並且預設值為 10 。 )
> T = array:new([1,{fixed,true},{default,10}]).
{array,1,0,10,10}
 ( 轉換為列表,直接看內容資料。預設值 10 被解讀為字元 \n 。 )
> array:to_list(T).
"\n"
 ( 取陣列第 0 索引值的元素。 )
> array:get(0, T).
10
> array:get(1, T).
** exception error: bad argument
     in function  array:get/2

由例子中可以學到,array:to_list/1 將陣列轉換為列表, array:get/2 對陣列取指定索引的元素。

陣列的更動[编辑]

array:set/3 是將指定的陣列元素賦值,三個參數依序為索引值、要賦與的值、以及陣列識別項。如果陣列是固定長度,給超出長度的索引賦值會失敗。如果陣列是變動長度,同樣的動作會使陣列長度增加。

 ( 延續上例,對索引值 1 的陣列元素賦值,結果會失敗。 )
> array:set(1, hello, T).
** exception error: bad argument
     in function  array:set/3
 ( 將陣列放寬為變動長度 。 )
> T1 = array:relax(T).
{array,1,10,10,10}
> array:set(1, hello, T1).
{array,2,10,10,{10,hello,10,10,10,10,10,10,10,10}}
> array:to_list(array:set(1,hello,T1)).
[10,hello]

在例子中的 array:relax/1 是將固定長度陣列改變為變動長度陣列。另外有 array:fix/1 是將變動長度陣列改變為固定長度陣列。

取得陣列屬性[编辑]

array:size/1 可讀取陣列長度。 array:is_fix/1 判斷陣列的固定性。 array:default/1 讀取陣列元素的預設值。

array:is_array/1 判斷參數是否為陣列,不過,這個函數是做比較粗略的判斷工作,並不會檢查陣列是否有良好格式。

字串模組[编辑]

Erlang 稱字串為整數列表,這表示字串擁有一部份列表的性質,而列表仍有許多字串所沒有的用途與用法。所以,對字串處理, Erlang 提供了 string 模組來幫忙。

一進入 string 模組的範圍,我們先看到 string:len/1 可以求字串長度。這與使用 erlang:length/1 求列表長度的功能相同。此外,你應該曉得,對字串處理,較明確的需求是:字串分割、合併,尋找子字串,取子字串,以及把字串轉為數字或其他類型等等。

字串分割與合併[编辑]

string:tokens/2 使用一些分隔符號把字串分割,分隔符號參數為一個或多個符號。 string:join/2 使用一些分隔符號將字串合併,分隔符號參數只代表一個分隔符號。

一個分隔符號的例子:

> string:tokens("hello, my world", ", .").
["hello","my","world"]

上例意思說是用逗號 "," 、空格 " " 或句號 "." 做分隔符號。字串分割結果是能將一般英文句子的字都拿出來,構成一個字串的列表。將來一旦考慮到一般英文句子的分解,你要想到冒號 ":" 、分號 ";" 以及單、雙引號的情況,不一定都只用 string:tokens/2 就能妥善處理,因為單引號也有做連寫詞彙的用法 ( this is —— this's ) ,而且把逗點放在右邊雙引號的前面是英文的慣例。

以下為合併字串的例子,只使用一個分隔符號:

> string:join(["hello","my","world"], "=v=").
"hello=v=my=v=world"

另外,有個 string:concat/2 可以將二個字串連接成一個字串:

> string:concat("hello,", " world").
"hello, world"

最後,有個獨特的 string:words/2 ,使用一個字元符號來告訴我們,會分解出多少個段落。例子如下:

> string:words("hello, my world").
3
> string:words("hello, my world", $m).
2

上例第一項呼叫 string:words/1 ,預設使用空格符號。第二項使用字母 m 為分隔符號,結果會有二個段落。 ( 在此要提醒,仔細閱讀 API 文件,不要對函數的字面望文生義。像這個 string:words/1 並沒有自動從句子中找出英文字這種聰明的功能,如果誤解了它的意思,可能會誤用。 )

子字串處理[编辑]

使用 string:str/2 可以判斷一個字串是否包含另一個字串,如果找到子字串就傳回子字串位置。需注意,如果沒有找到子字串,是傳回 0 。 找子字串是從左端開始找最靠左的子字串。同一個函數,有從右端找子字串的版本 string:rstr/2 ,是從右端開始找最靠右的子字串。

> string:str("hello, world", "hello, world!").
0
> string:str("hello, world", "world").
8
> string:str("coffe or tea or wine", "or").    
7
> string:rstr("coffe or tea or wine", "or").
14

至於擷取子字串, Erlang STDLIB 提供了 string:substr/x 和 string:sub_string/x 二種版本。二種版本分別又可有二個參數,或有三個參數。

string:substr/2string:sub_string/2 都是從字串中取指定開始位置到字串結尾的子字串。

> string:substr("hello, world", 7).
" world"
> string:sub_string("hello, world", 7).
" world"

string:substr/3 從開始位置取指定長度的子字串。

> string:substr("hello, world", 7, 4).
" wor"

string:sub_string/3 給一個開始位置、和一個結束位置,就取字串中這二個位置之間的子字串。

> string:sub_string("hello, world", 7, 11).
" worl"

另外,依之前介紹的另一個子字串取法 string:words/2 ,根據一個字元將字串分解為許多,還有個 string:sub_word/x 可以將所指定第幾個字取出。

string:sub_word/2 預設分段符號是空格:

> string:sub_word("hello, world", 2). 
"world"

string:sub_word/3 將字串用指定符號分段,然後取出指定的第幾個字:

> string:sub_word("hello, world", 2, $o).
", w"

字串轉換[编辑]

當字串內容含有數字時,可以用 string:to_integer/1string:to_float/1 轉換為數字。用法為,每呼叫一次,會將字串從頭開始找符合數字類型的子字串做轉換,並且留下剩下的部份。

> string:to_integer("1 2 3").  
{1," 2 3"}
> {Int,Rest} = string:to_integer("1 2 3"), string:to_integer(Rest).
{error,no_integer}

string:to_lower/1string:to_upper/1 用來做字串轉換小寫或大寫。

> string:to_upper("hello, world").
"HELLO, WORLD"

練習[编辑]

綜合您對 Erlang 資料型態、列表模組及字串模組的認知,請做以下練習:

分解計算式
寫一則函數,輸入字串內容為合理的四則運算式,函數會將式子的數字與符號分解:例如, (1024+512)*256 分解為 ["(","1024","+","512",")","*","256"] 。
計算式後序化
["(","1024","+","512",")","*","256"] 是一則中序計算式,可對應為後序式 ["1024","512","+","256","*"] 。寫一則函數,輸入表達為字串列表的中序式,將中序式轉換為後序式並輸出。您可以利用列表的基本操作恰好是堆疊的特性。
計算式求值
寫一則函數處理像 ["1024","512","+","256","*"] 這樣的後序式輸入。這個題目要用到堆疊,在遇到數字時放進堆疊,遇到運算符號就從堆疊中取二個數字求解,求解的結果再放入堆疊。如果計算式合理,最後堆疊只存有一個數字,就是解答。

輸入/輸出模組[编辑]

Erlang 標準程式庫有 io 模組提供輸入及輸出介面函數,以及 io_lib 模組為輸入及輸出所需要的字串處理提供相關函數。

常用的函數為 io:format/2 函數,指定格式與一些要處理的資料,就可以按照格式將文字印在標準輸出裝置。

> io:format("~s, ~s~n", ["hello","world"]).
hello, world
ok

以上例子, hello, world 為印出到標準輸出裝置的文字,而 ok 訊息是函數的執行結果。

io:format/1 可以印一般的文字訊息:

> io:format("hello, world~n").
hello, world
ok

在 io_lib 模組有一個對應的 io_lib:format/2 函數,是格式處理之後產出一列字元列表。例子如下:

> Result = io_lib:format("~wx~w=~w", [9, 8, 9*8]).
["9",120,"8",61,"72"]

所產出的文字可供做後續處理。

格式符號[编辑]

文字格式處理需要用許多格式符號,表示嵌入欄位或是特殊文字。以下列表簡述幾種常用的格式符號:

符號 說明
~~ 表示一個 ~ 文字。
~w 表示 Erlang 詞彙,可以印出原子及數字類型資料。
~.nB 表示以 n 為基數的 n 進制數字轉換。
~c 表示字元。
~s 表示字串。

格式符號都以 ~ 符號後接一個識別字母為主,有時在中間可以放一些調整參數:例如 ~10s 表示在十格空間中印一串文字,文字靠右。

檔案模組[编辑]

集合模組[编辑]

網路模組[编辑]

Mnesai 分散式資料庫[编辑]