erlang:世界是平行的!epaper.gotop.com.tw/pdfsample/axp012800.pdf · y...

Post on 26-Jul-2020

10 Views

Category:

Documents

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

譯者序

Erlang:世界是平行的!

如果你像我一樣每個月持續追蹤 Tiobe的編程語言需求排行榜,你應該也注

意到了,這兩個月名次竄升最猛烈的語言是 Erlang,一口氣約上升 15 個名

次,好像有一點開始紅的跡象。事實上,在電信產業,Erlang早就是最重要

的語言之一。

大多數的電信公司,都會用到 Erlang 來開發分散式系統,以達到共時

(Concurrent)與容錯(Fault-Torrent)的能力。除了共時與容錯,現在多核

心與 HT(超執行緒)的 CPU環境,也提供 Erlang語言相當好的發揮環境。

Erlang是由瑞典電信大學所創造出來的。後來移轉到 Ericsson(瑞典電信巨

擘)的 OTP(開放電信平台)實驗室,後來又被釋出,成為開放源碼的計畫。

所以,現在 Erlang不是任何公司的私有財產,這有助於 Erlang的推廣。

Erlang雖然和電信產業的淵源很深,但是 Erlang沒有劃地自限,用它來開發

財務系統或各種伺服器系統,也不少見。總之,只要想開發出分散式、多核

心、容錯、共時的系統,就相當適合使用 Erlang。

Erlang 提出一種所謂的「共時導向編程」( Concurrency-Oriented

Programming,COP)的概念,讓我們可以輕易地寫出共時的程式。共時、

平行(parallel)運算、多工(multi-task)、多執行緒(多線程),其實都是

一樣的概念,儘管各個語言或平台的作法可能不盡然相同。

iii 譯者序

如果我們想要寫程式,使它像真實世界的物件一樣地運作,那麼這樣的程式

就會用到共時的(concurrent)結構。如果使用「專為共時應用設計」的語

言,來開發這樣的應用,一切就會簡單許多。而目前最棒的共時編程語言,

應該就是 Erlang了。Erlang可以幫助我們,將我們思考和互動的方式,寫成

程式。

使用 Erlang開發系統的好處包括了:

寫出來的程式,移到多核心的環境中執行,速度會自然變快(甚至有

可能達到線性加速,n個核心就提升 n倍)。

可以寫出容錯的系統,當機之後會重新啟動。

可以寫出「程式碼熱抽換」的系統,可以一邊執行一邊升級,不用先

暫停服務。

寫出來的程式不可思議地精簡。

對 Erlang語言來說,共時是屬於語言的一部份,而不是作業系統的一部份。

Erlang讓平行編程變得容易,作法是將世界塑模(modeling)成一組平行的

行程(process),彼此之間只能透過訊息交換來互動(這裡所謂的行程,和

OS的行程不一樣)。在 Erlang的世界,有平行行程,但是沒有上鎖(lock)、

沒有同步化方法(synchronized method)、沒有共享記憶體而造成資料混亂

的可能。Erlang程式可以由數千甚至上百萬個極度質輕的行程所組成,這些

行程可以在一個多核心處理器上執行,或者在許多處理器所形成的網路上執

行。

Erlang提供了 Mnesia的 Erlang資料庫管理系統(DBMS)。Mnesia是整合

式的 DBMS,存取速度相當快。它可以被設定成跨越數個實體分離的節點進

行資料複製,以提供容錯的操作。Mnesia不是一般的 DBMS,它不使用 SQL

語言,而是使用自己的「list comprehension」語法進行資料庫的存取。

來一則軼事吧!Mnesia 這個名稱是有典故的,開發團隊最早為它取名為

amnesia(失憶症),但是老闆不同意:「資料庫有失憶症,成何體統」,

於是把字頭的 a去掉,變成 Mnesia。

Erlang程式設計 iv

除了 Mnesia之外,用 Erlang開發系統,也幾乎都會用到 OTP(開放電信平

台)程式庫。OTP 是一組 Erlang 程式庫與開放源碼程序,用來幫助 Erlang

程式建立工業等級的應用。儘管 OTP 的名稱有「電信」字眼,但用途是沒

有限制的。OTP是 Erlang威力的來源之一。利用 OTP寫出牢靠的伺服器是

相當容易的事。

和我之前學過的語言相比,Erlang是一個很古怪的語言,具有相當陌生的語

法,甚至還不支援物件導向。不只程式與眾不同,整個編程採用的手法也不

一樣。但是語法又相當簡潔有力。

我發現我很喜歡 Erlang 語言,因為它夠奇怪(太普通的語言,我是沒興趣

的)。沒想到 Erlang光是透過 pattern matching的語法,居然可以做這麼多

事,而且相當精簡;透過 messaging的方式,就可以輕易地做出多工、容錯、

分散式的系統;Erlang還大量地使用 higher-order function,已經到了無孔不

入的地步。

只要你的腦筋轉得過來,我覺得 Erlang算是相當精簡好學的語言。如果你有

分散式、容錯、多核心的開發需求,你應該將 Erlang列入考慮。過去這兩年,

我們品嚐過來自日本的 Ruby,現在你準備好品嚐下一道菜 – 來自北歐的

Erlang嗎?

蔡學鏞(Jerry Tsai)

April, 2008

開始學習

2.1 概觀

就和一般的學習一樣,在精通 Erlang的過程中,你也會經過一些階段。讓我

們看看本書涵蓋哪些階段,認清你一路上會體驗到的事有哪些。

第 1階段:我覺得陌生

身為一個初學者,你將要學習如何啟動系統,在 Shell 中執行命令,編譯簡

單的程式,然後漸漸熟悉 Erlang(畢竟 Erlang是個小型語言,所以這個階段

不會維持太久)。讓我們將此階段分成更細的數個小階段,身為初學者,你

將會做下面的事:

確定在你的電腦上有一套合用的 Erlang系統。

學習啟動與停止 Erlang Shell。

發現如何在 Shell中輸入表示式,估算(evaluate)表示式,且瞭解估

算結果。譯者註

知道如何使用你慣用的文字編輯器來建立並修改程式。

在 Shell中編譯並執行你的程式。

譯者註 估算、求值、執行,都是一樣的概念,可能會交替出現。

Erlang程式設計 8

第 2階段:我開始習慣

到目前為止,你將會具有此語言的一般常識。如果你遇到語言問題,你會具

有足夠的背景,可以從第 5章〈進階序列式編程〉中自行學習。

在此階段,你會熟悉 Erlang,可以進入更有趣的主題:

你會學到 Shell 的進階用法,比一開始時學到的更多(比方說,你可

以召回之前的表示式,以進行編輯,這樣的內容包含在 6.5 節〈在

Erlang Shell中進行命令編輯〉)。

你會開始學習程式庫(Erlang的程式庫稱為模組)。我所寫出來的程

式,大部分都可以只用到五個模組:lists、io、file、dict、gen_tcp;

因此本書大量使用這幾個模組。

隨著你的程式越來越大,你會學習到如何自動化編譯與執行。我們使

用的工具是 make。我們會看到如何透過寫 makefile來控制此過程。這

是第 6.4節〈用 Makefile進行自動編譯〉的內容。

Erlang 編程有一個更大的世界,使用一個名為 OTP1的大型程式庫集

合。當你的 Erlang經驗越來越多,你會發現 OTP可以讓你省下許多

時間,畢竟如果你想要的功能已經有人事先寫好了,你何需自己重頭

再做一次?我們會學習 OTP主要的行為,特別是 gen_server,這是

第 16.2節〈開始使用 gen_server〉的內容。

Erlang 主要的用途之一是寫出分散式(distributed)程式,現在是開

始體驗它的時機。你可以從第 10 章〈分散式編程〉的範例開始,將

這些範例擴充成你要的樣子。

第 2.5階段:我可能選擇多學一些

第一次閱讀本書的時候,你不需要逐章閱讀。Erlang和你之前使用過的大部

分語言不同,Erlang是 concurrent(共時)編程語言 — 這使得它相當適合用

1 開放電信平台(Open Telecom Platform)

9 Chapter 2 開始學習

來寫出分散式程式,適合現代多核心 CPU或 SMP2 電腦。只要使用多核心或

SMP的機器,大多數的 Erlang程式就會執行得更快(無須修改程式)。

Erlang 編程牽涉到使用一種我所謂的共時導向編程(concurrency-oriented

programming,COP)的編程範式(programming paradigm)。

當你使用 COP 時,你會將問題分解成小問題,在這些解決方案中找出自然

的共時。這是寫任何共時程式時,很重要的第一個步驟。

第 3階段:叫我 Erlang大師

現在你已經精通此語言,且可以寫出一些有用的分散式程式。但是想成為真

正的大師,你需要學更多:

Mnesia 資料庫。Erlang 軟體內建一個快速的資料庫,名為 Mnesia。

Mnesia 原本是設計給電信軟體使用,所以特別考慮到效能與容錯

(fault tolerance)的需求,現在即使是非電信軟體也常使用 Mnesia。

和其他編程語言之間如何銜接,如何使用連入驅動程式(linked-in

driver)。請見 12.4節〈連入驅動程式〉。

完整地使用 OTP建立行為的監督樹、開始劇本…等。請見第 18章〈用

OTP開發系統〉。

如何執行程式,如何為多核心電腦優化程式。請見第 20 章〈為多核

心 CPU編程〉。

最重要的一課

看本書時,你需要隨時記得一個規則:編程是好玩的。而我個人認為,編寫

分散式應用(像是聊天程式或即時通訊應用),更是比「我們慣常編寫的序

列式應用」有趣。你能在一部電腦上做的事相當有限,但是有了電腦網路,

2 SMP = 對稱多處理器(Symmetric multiprocessing)

Erlang程式設計 10

應用無限寬廣。Erlang提供了理想的環境,讓你可以實驗網路應用,建立出

具有實用品質的系統。

為了幫助你從一開始就能體會到這一點,我會在技術的章節內混合一些真實

世界的應用。你可以將這些應用當作你自行實驗的起點,用我料想不到的方

式修改它們、部署它們,我很樂見你這麼做。

2.2 安裝 Erlang

在你能做任何事之前,你必須確定你的電腦上已經安裝好 Erlang。打開命令

提示字元(也就是 console視窗),鍵入 erl,如下所示:

$ erl

Erlang (BEAM) emulator version 5.5.2 [source] ... [kernel-poll:false]

Eshell V5.5.2 (abort with ^G)

1>

在 Windows作業系統上,必須安裝好 Erlang,且做好 PATH環境變數的設

定,才會成功。假設你用標準的作法安裝程式,你將可以透過「開始」>

「所有程式」>「Erlang OTP」選單,取用 Erlang。在附錄 B,我會告訴

你如何操控 Erlang,讓它和 MinGW與 MSYS搭配執行。

注意:我只有偶而才會展示此標語(也就是『Erlang (BEAM) ... (abort with

^G)』這段文字)。只有當你想要提出 bug 報告時,此資訊才會派上用場。

我在此展示它,好讓你第一次看到它時不會太驚訝,不知道這是什麼。大多

數的時候,我會將它省略不顯示,除非真的有必要顯示。

如果你看到此 shell標語,就表示 Erlang確實有安裝在你的電腦上,先離開

它(按下 Ctrl+G,後面跟著字母 Q,然後按下 Enter或 Return)3。現在你可

以跳到 2.3節〈本書的程式碼〉。

3 或者在 shell中使用 q()命令。

11 Chapter 2 開始學習

如果你得到的是錯誤訊息,告訴你 erl是未知的命令,就表示你需要先安裝

Erlang。這個時候你就需要做一個決定 — 你想使用預先建置好的二元版本?

或使用包裝好的版本(OS X)?或自行編譯 Erlang 的源碼?或使用

Comprehensive Erlang Archive Network(CEAN)?

二元版本

Erlang準備有 Windows和 Linux的二元散佈版本,安裝二元版本的方式會因

為 OS不同而有差異,讓我們分別解說。

Windows

你會在 http://www.erlang.org/download.html看到一個清單。選擇最新版本,

選擇 Windows二元檔的連結(指向 Windows執行檔)。點擊此連結,然後

依照指示動作,這是標準的 Windows 安裝方式,所以過程應該不會有什麼

問題。

Linux

Linux版本的二元檔案相容於 Debian系統(以及其衍生系統),使用下面的

命令:

> apt-get install erlang

Mac OS X

如果你使用 Mac,你可以利用 MacPorts 系統來安裝預先建置好的 Erlang,

或者你可以自行編譯源碼。使用 MacPorts 是最簡單的作法,且會自行處理

版本更新。然而,MacPorts可能會有一點版本落後的狀況發生,比方說,在

我開始寫本書的時候,Erlang的 MacPort版本比最新版落後兩個版本。為此,

我建議你不如忍受一時的痛苦,自行編譯 Erlang源碼,下一節會告訴你怎麼

做。要這麼做,你必須確定你已經事先具有開發工具(在隨機附上的光碟片

中,有這樣的工具軟體)。

Erlang程式設計 12

從源碼建置 Erlang環境

如果不進行二元安裝,另一種作法是從源碼建立 Erlang。對於 Windows 來

說,這麼做其實沒有什麼優點,因為只要有新版本就會提供二元檔。但是對

於 Mac和 Linux平台來說,二元檔可能會來得晚一些。對於任何 Unix類的

OS,安裝方法都是一樣的:

1. 取得最新的 Erlang 源碼 4。源碼會被包在一起,檔案名稱類似

otp_src_R11B-4.tar.gz(表示 Erlang第 11版的第 4個維護釋出版本)。

2. 解開打包、設定組態、make成二元檔、安裝。作法如下: $ tar -xzf otp_src_R11B-4.tar.gz

$ cd otp_src_R11B-4

$./configure

$ make

$ sudo make install

注意:在建置系統前,你可以使用「./configure --help」命令,來得知可

用的組態選項有哪些。

使用 CEAN

CEAN(Comprehensive Erlang Archive Network)試圖將所有主要的 Erlang

應用集中在一地,具有共同的安裝程式。使用 CEAN的優點是,它不只管理

基本的 Erlang系統,也具有用 Erlang開發出來的許多包裹(Package)。這

意味著除了可以保持 Erlang環境在最新版,也可以一併維護這些包裹。CEAN

預先為各種 OS和 CPU編譯好各種二元檔。想利用 CEAN來安裝系統,使

用 http://cean.process-one.net/download/,並依照指示去做。(請注意,有些

讀者回報 CEAN 可能不會安裝 Erlang 編譯器,如果這樣的情況發生在你身

上,那麼啟動 Erlang shell,使用命令「cean:install(compiler)」,就可以

安裝編譯器。)

4 http://www.erlang.org/download.html

13 Chapter 2 開始學習

2.3 本書的程式碼

我們展示過的大多數程式碼片段,都是取自完整的可執行範例,你可以下載

這些範例5。如果是出自可以下載的程式,程式上方就會出現一個橫條(如下

所示):

下載 shop1.erl

-module (shop1).

-export ([total/1]).

total([{What, N}|T]) -> shop:cost(What) * N + total(T);

total([]) -> 0.

此橫條包含程式碼的路徑。如果你閱讀本書的 PDF版本,且你的 PDF閱覽

器支援超連結,則你可以直接點擊此橫條,程式就會出現在一個瀏覽器的視

窗上。

2.4 啟動 Shell

現在讓我們開始吧!我們可以使用 Shell和 Erlang進行互動。一旦我們啟動

Shell,我們可以鍵入表示式(expression),且 Shell將會顯示出表示式的值。

如果你已經在系統上安裝了 Erlang(依照 2.2節〈安裝 Erlang〉的描述),

那麼此 Erlang Shell(也就是 erl)也就被安裝好了。想執行它,打開你慣用

的 OS Shell,(例如 Windows的 cmd或 Unix系統的 bash),在命令提示符

號下,鍵入 erl,就可以啟動 Erlang Shell:

$ erl

Erlang (BEAM) emulator version 5.5.1 [source] [async-threads:0] [hipe]

Eshell V5.5.1 (abort with ^G)

1> % I'm going to enter some expressions in the shell ..

1> 20 + 30.

50

2>

5 http://pragmaticprogrammer.com/titles/jaerlang/code.html

Erlang程式設計 14

讓我們看看,我們剛剛做了什麼:

這是 Unix命令,用來啟動 Erlang Shell,回應會告訴我們 Erlang是那個

版本。

Shell會印出提示符號 1>,然後我們鍵入一個註解(comment)。百分比

(%)字元表示開始進入註解,一直到該行結束。註解內容會被 Erlang編

譯器忽略不理會。

因為命令尚未鍵入完畢,所以提示符號「1>」重複出現,現在我們鍵入

表示式「20 + 30」,後面跟著一個英文句號和一個換行。(初學者常常

會忘了鍵入句號,這會導致 Erlang不知道我們的表示式已經完畢,所以

就不會看到結果被顯示出來。)

Shell會計算出表示式,然後印出結果(本範例的結果是 50)。

Shell印出另一個提示符號,這次命令編號是 2(每個新命令的編號都會

遞增)。

你在自己的電腦上試著執行過此 Shell 嗎?如果還沒有試過,現在就請你動

手做。光看書不動手,你可能會以為這樣你還是可以瞭解,但是你卻無法將

知識從腦袋轉移到手指 — 編程(編寫程式,programming)可不是觀賞運動

比賽,而比較像是參加運動比賽,多練習才會進步。

先鍵入一模一樣的表示式,然後試著修改做一些實驗。如果行不通,停下腳

步,問問自己,到底錯在哪裡。即使經驗豐富的 Erlang編程員也會花許多時

間和 Shell做互動。

隨著你越來越有經驗,你會發現此 Shell 是威力很強的工具。之前的 Shell

命令可以被召回(Ctrl+P和 Ctrl+N)並編輯修改(利用類似 emacs的編輯命

令)。這是 6.5節〈Erlang Shell命令編輯〉的內容。最棒的是,當你開始寫

分散式程式,你會發現你可以將一個 Shell依附(attach)到叢集(cluster)

內不同 Erlang節點(node)執行中的 Erlang系統,或用保全的 Shell(ssh)

連線直接連接到遠端電腦上執行的 Erlang 系統。你可以和任何 Erlang 節點

上的任何程式互動。

15 Chapter 2 開始學習

警告:你無法將本書讀到的一切都鍵入 Shell。特別是,你無法將「列在

Erlang程式檔案中的程式碼」鍵入 Shell。在.erl檔案內的語法格式不是

表示式,所以 Shell 不瞭解。Shell 只能估算 Erlang 表示式,除此之外,

一律不懂。特別是,你不可以把模組註記(module annotation)鍵入 Shell

(也就是那些以減號開始的東西,例如-module、-export)。

本章剩下的部分,是許多和 Erlang Shell的簡短對話。我並不會解釋所有的

細節,因為這會將內文的流程打亂。在第 5.4節〈許多簡短主題〉,再說明

許多之前略過的細節。

2.5 簡單整數算術

讓我們計算一些算術表示式:

1> 2 + 3 * 4.

14

2> (2 + 3) * 4.

20

重要:你會看到這些對話都是以命令編號 1開始(也就是 Shell會印出「1>」)。

這意味著我們重新開始一個新的 Erlang Shell。每次你看到一個對話盒時,

只要一開始顯示「1>」,你都必須啟動新的 Shell(如果你想要確實重現本

書範例的環境)。當範例一開始的命令編號大於 1,這表示此 Shell會談是從

前面的範例接續下來的,因此你不需要啟動新的 Shell。

注意:如果你想要一邊閱讀本書,一邊在 Shell 鍵入這些範例(這當然是值

得鼓勵的行為),那麼你可能想要很快地看一下第 6.5 節〈在 Erlang Shell

中進行命令編輯〉。

你會看到 Erlang遵守正常的算術表示式規則,所以「2 + 3 * 4」是「2 + (3

* 4)」的意思,而不是「(2 + 3) * 4」。

Erlang使用任意長度的整數,來進行整數算術。在 Erlang中,整數算術是精

確的,因此你不用擔心算術溢位(overflow)的問題。

Erlang程式設計 16

Shell沒有回應嗎?

如果在你鍵入一個命令之後,Shell沒有回應,那麼你可能忘了鍵入「點

空白」(dot-whitespace),也就是一個英文句號後面跟著一個 Return。

另一件可能犯的錯是你想前後括上引號(單引號或雙引號),卻前後使

用不一致的引號。

上述任何一件事情發生,那麼最好鍵入多的一個關閉引號,後面跟著「點

空白」。如果狀況真的變得很糟糕,且系統完全沒有回應,那麼鍵入

Ctrl+C(在 Windows上,鍵入 Ctrl+Break)。你會看到下面的輸出:

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded

(v)ersion (k)ill (D)b-tables (d)istribution

現在按下 A,表示你要放棄(abort)目前的 Erlang會談(session)譯者註

進階:你可以啟動與結束多重 Shell,見 6.7節〈Shell沒有回應〉的詳

細說明。

何不試試看?你可以計算相當大的數字,讓你的朋友對 Erlang印象深刻:

3> 123456789 * 987654321 * 112233445566778899 * 998877665544332211.

13669560260321809985966198898925761696613427909935341

鍵入整數的方法有許多種6。下面是一個表示式,使用到基底 16 和基底 32

的記號法:

4> 16#cafe * 32#sugar.

1577682511434

2.6 變數

你要如何儲存命令的結果,供以後使用?這時候你需要變數,請看範例:

1> X = 123456789.

123456789

發生什麼事了?首先,我們將一個值指定給變數 X;然後 Shell 會印出此變

數的值。

譯者註 Session翻譯成「會談」,表示:一個程式的操作期間。 6 見 5.4節對整數的討論。

17 Chapter 2 開始學習

變數記號法

常常我們想要討論特定變數的值。為此我們使用 Var Value 的記號

法,比方說,「A 42」表示變數 A 的值是 42。如果有數個變數,我

會寫成「{A 42, B true ... }」,表示 A是 42,B是 true…等等。

注意:所有的變數名稱必須用大寫字母開始。如果你想要看到變數的值,只

要輸入變數名稱即可:

2> X.

123456789

現在 X具有一個值了,你可以使用 X:

3> X*X*X*X.

232305722798259244150093798251441

然而,如果你試圖指定(assign)不同的值到變數 X,你會得到錯誤訊息:

4> X = 1234.

=ERROR REPORT==== 11-Sep-2006::20:32:49 ===

Error in process <0.31.0> with exit value:

{{badmatch,1234},[{erl_eval,expr,3}]}

** exited: {{badmatch,1234},[{erl_eval,expr,3}]} **

發生什麼事?想解釋,我就必須破除你對於「X = 1234」所做的兩個假設:

首先,X不是一個變數,至少不是像你在 Java和 C中所慣用的那樣。

第二,=不是指定運算子(assignment operator)。

對於 Erlang新手來說,這是最詭異的地方之一。讓我們花幾頁的篇幅,討論

更深入一些。

Erlang程式設計 18

單一指定就像是代數

當我在學校唸書時,我的數學老師說過『如果一個等式中,X出現多次,

那麼所有 X代表的是相同的值』,我們就是因為這樣才能解開方程式:

如果我們知道「X+Y=10」,且「X-Y=2」,那麼 X就會是 6,且 Y就

會是 4。

但是當我們學習第一個編程語言時,我們看到像下面這樣的式子:

X = X + 1

大家都搖頭,直說『這怎麼可能!』但是老師說我們錯了,我們必須暫

時把數學課學過的知識忘掉,X不是一個數學變數:它只是一個鴿子洞,

或者是一個小盒子........

在 Erlang中,變數就像是數學中的變數。當你將值和變數關連上,你其

實是在做聲明(assertion),也就是陳述一件事實:變數具有此值。就

是這樣。

不會變的變數

Erlang具有「單一指定」變數。顧名思義,單一指定變數只能夠被賦予值一

次。如果你試圖改變「已經被設定過」的變數值,你就會得到錯誤(事實上,

你會得到一個 badmatch 錯誤,如同上面的例子所示)。「具有指定值」的

變數,被稱為「已繫結」變數(bound variable);未具有值的變數,稱為「未

繫結」(unbound)變數。變數一開始都是未被繫結的。

當 Erlang看到像「X = 1234」這樣的敘述,它會將變數 X繫結到值 1234。在

被繫結之前,X 可以具有任何值:X 只是一個洞,等待被填入資料。然而,

一旦得到值,就會永遠持有。

此時你可能會納悶,為何我們稱呼這樣的東西為變數(variable),其實是有

兩個原因的:

它們是變數,不過只能被改變一次(也就是從「未繫結」到「具有一

個值」)。

19 Chapter 2 開始學習

它們看起來像是一般編程語言的變數,所以當我們看到像「X = ...」

這樣的程式碼,我們的腦袋會說『我知道這是什麼;X 是一個變數,

且=是指定運算子』。這樣的看法幾乎是對的:X 幾乎是變數,且=幾

乎是指定運算子。

注意:在 Erlang程式碼中使用省略記號(...),表示我有省略一些程

式碼。

事實上,=是「模式比對」運算子,如果 X是未繫結變數,則=的行為就是一

般的指定。

最後,變數的範疇(scope)是「定義它的時候」所在的語彙單元。所以

如果 X 被用在單一函數條款(clause)內譯者註

,它的值不會『逃脫』到函

數條款外部。相同函數內的不同條款,共享所謂的全域與私用變數,對

Erlang來說是不可能的。如果許多不同函數內都有 X,那麼這些 X的值都

會不一樣。

模式比對

對於大多數的語言來說,=表示要指定變數的值。然而對 Erlang來說,=是指

「模式比對」(pattern matching)運算。Lhs = Rhs的意思是:估算右側(Rhs),

然後和左側(Lhs)比對結果。

現在變數(例如 X)是模式的一種簡單形式。我們之前也說過,變數只能被

給予值一次,當我們第一次說 X = SomeExpression的時候,Erlang會自問:

『如何才能讓此敘述成真?』因為 X 尚未有值,所以 Erlang 就把 X 繫結到

SomeExpression的值,然後此敘述就會變得正當(valid),皆大歡喜。

如果之後我們說 X = AnotherExpression,那麼只有當 SomeExpression 與

AnotherExpression完全等同時,才會成功。下面是一個範例:

譯者註 下一章會解釋何謂條款(clause)。

Erlang程式設計 20

1> X = (2+4).

6

2> Y = 10.

10

3> X = 6.

6

4> X = Y.

=ERROR REPORT==== 27-Oct-2006::17:25:25 ===

Error in process <0.32.0> with exit value:

{{badmatch,10},[{erl_eval,expr,3}]}

5> Y = 10.

10

6> Y = 4.

=ERROR REPORT==== 27-Oct-2006::17:25:46 ===

Error in process <0.37.0> with exit value:

{{badmatch,4},[{erl_eval,expr,3}]}

7> Y = X.

=ERROR REPORT==== 27-Oct-2006::17:25:57 ===

Error in process <0.40.0> with exit value:

{{badmatch,6},[{erl_eval,expr,3}]}

事情是這樣的:在第 1 行,系統執行表示式 2+4,得到答案是 6。這一行之

後,Shell會做下面的繫結設定:{X 6}。第 3行之後,繫結會變成{X 6,

Y 10}。

現在我們到第 5行。就在我們估算此表示式之前,我們知道 X 6,所以比

對 X = 6會成功。

當我們在第 7行說 X = Y,我們的繫結是{X 6, Y 10},因此比對失敗,

印出錯誤訊息。

第 4到第 7行的表示式,會成功或失敗都是根據 X與 Y的值而定。現在的時

機適合好好思考這一切,確定你真的瞭解之後,再繼續閱讀下去。

你可能會認為我太過於執著討論這一點,所有的模式中「=」左邊都只是變

數(不管有無繫結),但是我們稍後會看到,我們可以做出相當複雜的模式,

然後用「=」進行比對。在介紹過值組(tuple)與清單(list)(都是用來儲

存複合資料項目)之後,我們會回到這個主題。

1

-

-

-

5

-

-

-

-

10

-

-

-

-

15

-

-

-

-

20

21 Chapter 2 開始學習

為何單一指定會讓程式更好?

在 Erlang中,變數只是某個值的一個「參考」(reference),也就是說,在

Erlang內部的實踐,被繫結變數其實是一個指標,指向真正儲存值的位置,

而此值是不能被改變的。

值不能被改變,這一點相當重要,這和 Java或 C這類命令式語言(imperative

language)的作法完全不同。

讓我們看看當你被允許改變變數時,會發生什麼事。先讓我們定義一個變數 X:

1> X = 23.

23

現在我們可以在運算中使用 X:

2> Y = 4 * X + 3.

95

現在假設我們可以改變 X的值(好恐怖):

3> X = 19.

幸好 Erlang不允許我們這麼做。Shell對此瘋狂之舉提出警告:

=ERROR REPORT==== 27-Oct-2006::13:36:24 ===

Error in process <0.31.0> with exit value:

{{badmatch,19},[{erl_eval,expr,3}]}

意思是,X已經是 23,所以不能是 19。

假設我們可以這麼做,那麼 Y的值會是錯的,因為它不滿足第二個敘述。且

如果 X可以在程式中不同的點有不同的值,而某地方出錯了,那麼,到底是

那個 X的值造成錯誤?到底是那個點得到這個出錯的 X值?這些問題都會變

得很難以回答。

在 Erlang中,只要設定之後,變數值就無法改變。這可以幫助除錯的簡化。

想瞭解為何會如此,我們必須好好地思考,什麼是錯誤,且錯誤要如何被

發現。

Erlang程式設計 22

沒有副作用,這意味著我們可以將程式平行化處理

可以被修改的記憶體,術語稱為「可變狀態」(mutable state)。Erlang

是一個函數式(functional)編程語言,且具有「非可變狀態」。

本書後面,我們會看看如何編寫多核心 CPU 的程式。當面對多核心編

程時,「非可變狀態」所帶來的好處,是相當大的。

如果你使用一個傳統的編程語言,像是 C或 Java來處理多核心 CPU,

那麼你需要對付記憶體共享的問題。為了不要敗壞(corrupt)共享的記

憶體,使用記憶體時,必須將記憶體上鎖。像 C或 Java這種存取共享

記憶體的程式,在處理共享記憶體時,要很小心,不然會當機。

在 Erlang中,沒有易變狀態,沒有共享記憶體,不需要上鎖。這使得程

式的平行化變得相當容易。

你會發現程式不正確,可能是因為某個值和你預期的不同。如果是這樣,那

麼你必須當程式維持在不正確值時,發現這件事。如果此變數改變值多次,

且分佈在許多時間點,那麼找出確切「值不正確」的地方會相當困難。

對於 Erlang來說,沒有這樣的問題。變數只能被設定一次,然後就再也不會

被改變了。所以一旦我們知道哪個變數是不正確的,我們就可以立刻推斷出

此變數被繫結的地方,一定是發生錯誤的地方。

此時,你可能會懷疑,程式不用到變數,怎麼可能?要如何表示像「X = X +

1」這樣的式子?答案很簡單,創造一個新的變數,其名稱尚未被使用過(比

方說 X1),然後就可以寫出「X1 = X + 1」。

2.7 浮點變數值

讓我們試圖做一些浮點算術:

1> 5/3.

1.66667

2> 4/2.

2.00000

3> 5 div 3.

1

23 Chapter 2 開始學習

4> 5 rem 3.

2

5> 4 div 2.

2

6> Pi = 3.14159.

3.14159

7> R = 5.

5

8> Pi * R * R.

78.5397

不要混淆了。在第 1行的最後是整數 3,不是小數 3,最後的點是表示式

的句號而不是小數點。如果我要寫浮點數,會寫成 3.0。

「/」一定會傳出浮點數;因此 4/2得到 2.0000(在 shell中)。「N div M」

且「N rem M」分別是用來做整數的除法與求餘數;因此「5 div 3」是 1,

而「5 rem 3」是 2。

浮點數必須包含一個小數點,且小數點之後一定要有至少一個十進位數字。

當你用「/」將兩個整數相除,結果會自動轉成浮點數。

2.8 原子

在 Erlang,原子(atom)用來表示不同的非數字常數值。

如果你習慣 Java或 C的列舉型別,那麼其實你已經在使用非常類似於 atom

的東西了,儘管你不見得有意識到這一點。

C編程員都很熟悉,用符號常數可以讓程式容易閱讀理解。通常 C程式會在

一個引入檔(include file)內定義一些全域(global)常數,由許多常數定義

所組成;比方說,glob.h可能包含了:

#define OP_READ 1

#define OP_WRITE 2

#define OP_SEEK 3

...

#define RET_SUCCESS 223

...

Erlang程式設計 24

通常 C的程式碼用下面的方法來使用這些符號常數:

#include "glob.h"

int ret;

ret = file_operation(OP_READ, buff);

if ( ret == RET_SUCCESS ) { ... }

在 C程式碼中,這些常數的值並不是很有趣;唯一有趣的地方,是因為它們

都不一樣,且可以比較是否相等。

此程式的 Erlang對等版本看起來是這樣的:

Ret = file_operation(op_read, Buff),

if

Ret == ret_success ->

...

在 Erlang中,原子是全域的,且不需要使用巨集定義或引入檔案,就可以

辦到。

假設你想要寫出一個程式,處理星期幾。你要如何在 Erlang中表達星期的某

一天,當然你可以使用原子 monday、tuesday…。

原子是以小寫字母開始,後面跟著一序列的英數(alphanumeric)字元,或

底線(_),或@符號7。比方說:red、december、cat、meters、yards、

joe@somehost、a_long_name。

原子也可以用單引號包圍。使用括引號的格式,我們可以建立出「一開始是

大寫字母」的原子,或者「包含非英數字元」的原子。比方說:'Monday'、

'Tuesday'、'+'、'*'、'an atom with spaces'。你甚至可以將不需要括引

號的原子也括上引號,所以'a'的意義和 a完全一樣。

原子的值就是此原子。所以如果你把一個原子當作命令,Erlang Shell會印出

該原子的值:

1> hello.

hello

7 你可能會發現英文句號(.)也可以用來當作原子,但 Erlang不建議你這麼做。

25 Chapter 2 開始學習

討論原子的值就和討論整數的值一樣,似乎有一點怪。但是因為 Erlang是一

個函數式(functional)編程語言,每個表示式都會有一個值,整數和原子當

然也是一種「表示式」,簡單到極致的表示式。

2.9 值組

假設你想要將固定數目的項目組成單一個值,那麼你就需要用到 tuple(值

組)。你可以用大括號建立一個值組,其內的項目用英文逗號隔開。比方說,

如果你想要表示某人的名字和身高,你可以寫成{joe, 1.82}。這就是一個

值組,包含一個原子和一個浮點數。

值組類似於 C的結構(struct),差別在於值組是匿名的(anonymous)。在

C中,型別 point的變數 P可以宣告如下:

struct point {

int x;

int y;

} P;

你利用點(.)運算子存取 C結構的欄位。要設定 point 中 x 與 y 的值,就

這麼寫:

P.x = 10; P.y = 45;

Erlang不具有型別宣告,所以想建立一個 point,這麼寫就可以了:

P = {10, 45}

這建立了一個值組(tuple),且將它的值繫結到變數 P。不同於 C,值組的

欄位不具有名稱,因為值組本身只包含兩個整數,我們必須記得它們各自的

用途。想記得一個值組的用途,習慣上會在值組內第一個位置插入一個原

子,用來描述該值組是什麼。所以我們會寫出{point, 10, 45},而非{10,

45},這使得程式更好理解一些8。

8 這種貼標籤的方式不是 Erlang強制的作法,而是普遍推薦的編程風格。

Erlang程式設計 26

值組可以套疊(nest)。假設我們想要展現某人的一些資料(名字、身高、

足長、眼睛的顏色)。我們可以這麼做:

1> Person = {person,

{name, joe},

{height, 1.82},

{footsize, 42},

{eyecolour, brown}}.

注意,我們這裡使用原子來識別欄位(名字與眼睛顏色),同時當欄位的值。

建立值組

當宣告時,值組會自動建立;當不需要時,值組會自動毀滅。Erlang使用垃

圾收集器,來回收不用的記憶體,所以我們可以不用擔心記憶體配置的問題。

如果你使用一個變數來建立一個新的值組,那麼此新值組將會共用「此變數

所參考到的資料結構」的值,請看範例:

2> F = {firstName, joe}.

{firstName,joe}

3> L = {lastName, armstrong}.

{lastName,armstrong}

4> P = {person, F, L}.

{person,{firstName,joe},{lastName,armstrong}}

如果你試圖建立一個資料結構具有未定義的變數,那麼你就會得到錯誤訊

息。所以在下一行,如果你試圖使用尚未定義的變數 Q,會得到錯誤:

5> {true, Q, 23, Costs}.

** 1: variable 'Q' is unbound **

這表示變數 Q是沒有定義的。譯者註

從值組中取出值

稍早,我們說過=看起來像是指定運算子,但其實是模式比對運算子。你可

能懷疑為何我們要這樣賣弄學問,這是因為「模式比對」是 Erlang的重要基

譯者註 提醒你,原子開頭字母小寫,變數開頭字母大寫。這是 Erlang強制的語法。

27 Chapter 2 開始學習

礎,且用在許多不同的任務上:「從資料結構中取出值」會用到它;「函數

內的流程控制」會用到它;當你送出訊息到一個行程(process)中,選取要

處理的訊息,也會用到它。

如果我們想要從一個值組中取出一些值,我們會使用=模式比對運算子。

讓我們看看代表 point的值組:

1> Point = {point, 10, 45}.

{point, 10, 45}.

假設我們想要取出 Point的欄位,分別放進 X與 Y,我們的作法是:

2> {point, X, Y} = Point.

{point,10,45}

3> X.

10

4> Y.

45

在命令 2中,X 被繫結到 10,Y 被繫結到 45。此表示式「Lhs = Rhs」的值

被定義為 Rhs,所以 Shell會印出{point,10,45}。

如你所見,等號兩邊的值組必須具有相同個數的元素,且對應的元素必須繫

結到相同的值。

現在假設你輸入一些東西,像是:

5> {point, C, C} = Point.

=ERROR REPORT==== 28-Oct-2006::17:17:00 ===

Error in process <0.32.0> with exit value:

{{badmatch,{point,10,45}},[{erl_eval,expr,3}]}

發生什麼事了?模式{point, C, C}和{point, 10, 45}無法符合比對,因為

C不可以同時為 10和 45。因此,模式比對失敗9,且系統印出錯誤訊息。

9 熟悉 Prolog的讀者請注意:Erlang認為比對不合就是失敗,且不沿原路返回。

Erlang程式設計 28

如果你具有一個複雜的值組,那麼你從值組中取出值的作法,是寫出一個模

式,其形狀(也就是結構)和值組一致,且在你想取出值的地方使用未繫結

的變數10。

想解說這一點,我們先定義一個變數 Person,它包含一個複雜的資料結構:

1> Person={person,{name,{first,joe},{last,armstrong}},{footsize,42}}.

{person,{name,{first,joe},{last,armstrong}},{footsize,42}}

現在我們寫一個模式來取出此 person的第一個名字:

2> {_,{_,{_,Who},_},_} = Person.

{person,{name,{first,joe},{last,armstrong}},{footsize,42}}

最後,我們印出 Who的值:

3> Who.

joe

請注意,在前面的範例中,遇到我們不感興趣的變數,我們寫一個「_」,

此符號稱為匿名變數(anonymous variable)。和正常變數不同的是,同一個

模式中_出現多次,不必繫結到相同的值。

2.10 清單

我們使用清單(list)來儲存變動個數的東西,例如:你的採購項目、星球的

名稱、質數函數會傳出的值…。

我們利用中括號,就可以建立清單,括號內的元素用逗號隔開。如下面的購

物清單所示:

1> ThingsToBuy = [{apples,10},{pears,6},{milk,3}].

[{apples,10},{pears,6},{milk,3}]

清單的個別元素可以是任何型別,比方說下面的例子:

2> [1+7,hello,2-2,{cost, apple, 30-20},3].

[8,hello,0,{cost,apple,10},3]

10 使用模式比對取出變數的方法,稱為 unification(統一化)。在許多函數式與邏輯式編程語言中,都看得到 unification。

29 Chapter 2 開始學習

術語

我們稱清單的第一個元素是清單的頭(head)。想像從清單中移除頭,剩下

的部分稱為清單的尾(tail)。比方說,如果我們具有一個清單[1,2,3,4,5],

然後清單的頭是整數 1,則清單的尾是[2,3,4,5]。注意清單的頭可以是任何

東西,但是尾一定也是一個清單。

取用清單的頭,效率相當高,所以幾乎所有的清單處理函數都是先取出清單

的頭,然後對它做一些事,然後才處理清單的尾。

定義清單

如果 T 是一個清單,那麼[H|T]也是一個清單11,其頭是 H,且尾是 T。垂直

線「|」用來區隔頭和尾。而[ ]是一個空的清單。

當我們使用[...|T]語法建立清單時,我們應該確定 T 是一個清單。如果是

的話,那麼此新的清單就是『具有適當的格式』的。如果 T不是一個清單,

則新的清單會被認為『格式不適當』。大多數的程式庫函數假定清單是格式

適當的,且無法處理格式不適當的清單。

我們可以寫[E1,E2,..,En|T],加入更多元素到 T的頭,比方說:

3> ThingsToBuy1 = [{oranges,4},{newspaper,1}|ThingsToBuy].

[{oranges,4},{newspaper,1},{apples,10},{pears,6},{milk,3}]

從清單中取出元素

我們可以利用模式比對運算子,從清單中取出元素。如果我們具有非空清單

L,那麼表示式「[X|Y] = L」(其中 X與 Y是未繫結變數)會將清單的頭放

到 X,將清單的尾放到 Y。

11 LISP編程員請注意:[H|T]是一個 CONS cell,具有 CAR H與 CDR T。在模式中,此語法會將 CAR與 CDR拆開。在表示式中,會建構出一個 CONS cell。

Erlang程式設計 30

我們已經到商店了,我們具有一個購買清單 ThingsToBuy1,第一件事,就是

打開清單,分成頭與尾兩部份:

4> [Buy1|ThingsToBuy2] = ThingsToBuy1.

[{oranges,4},{newspaper,1},{apples,10},{pears,6},{milk,3}]

成功地繫結起:

Buy1 {oranges,4}

並繫結起:

ThingsToBuy2 [{newspaper,1}, {apples,10}, {pears,6}, {milk,3}]

我們要先買 oranges,然後就可以取出下面的項目:

5> [Buy2,Buy3|ThingsToBuy3] = ThingsToBuy2.

{newspaper,1},{apples,10},{pears,6},{milk,3}]

成功地讓 Buy2 {newspaper,1},且讓 Buy3 {apples,10},還有

ThingsToBuy3 [{pears,6},{milk,3}]。

2.11 字串

嚴格說來,Erlang沒有字串,字串其實只是許多整數構成的一個清單。字串

放在一對雙引號內,像下面這樣:

1> Name = "Hello".

"Hello"

注意:在某些編程語言中,字串可以用單引號或雙引號當括號,但是在 Erlang

中,我們只能使用雙引號。

"Hello"只是整數清單的速寫方式,每個整數是個別字元的編碼。

當 Shell 要印出此清單的值,會將它當作字串印出。但是只有當清單中的全

部的整數都是可印字元時譯者註

,才會如此。請看範例:

譯者註 Erlang只有考慮到 ASCII的可見字元,並不把其他國家的語言編碼考慮在內。

31 Chapter 2 開始學習

2> [1,2,3].

[1,2,3]

3> [83,117,114,112,114,105,115,101].

"Surprise"

4> [1,83,117,114,112,114,105,115,101].

[1,83,117,114,112,114,105,115,101].

表示式 2中,清單[1,2,3]會被直接印出,沒有轉成字串。這是因為 1、2、3

都是不可印字元。

表示式 3的所有項目都是可印字元,所以會印成字串。

表示式 4就和表示式 3一樣,除了清單一開始是 1之外。但是 1是不可印字

元,所以會直接印出,不轉換成字串。

我們不需要知道,哪一個整數代表某個特定的字元。我們可以使用『錢號語

法』來達到此目的。比方說,$a是一個整數,代表一個字元 a。

5> I = $s.

115

6> [I-32,$u,$r,$p,$r,$i,$s,$e].

"Surprise"

字串使用的字元集

字串使用的字元是屬於 Latin-1(ISO-8859-1)字元編碼。比方說,字串包含

瑞典名字「Håkan」,會被編碼成[72,229,107,97,110]。

注意:如果你輸入[72,229,107,97,110]當作 Shell表示式譯者註

,你可能不會

得到你所期望的:

1> [72,229,107,97,110].

"H\345kan"

發生什麼事?這其實和 Erlang 沒有關係,而是和你的終端機(terminal)設

定有關係。

譯者註 在正體中文環境中,你可能會看到的是「"H嶡 an"」而非「"H\345kan"」。

Erlang程式設計 32

Erlang 只知道,字串是一個由整數構成的清單,這些整數代表某種編碼

(encoding)。如果剛好是可印的 Latin-1碼,就會正確的地顯示出來(當然

也必須終端機設定正確才行)。

2.12 再談模式比對

想要讓本章圓滿結束,我們必須再次回到模式比對。

下面的表格具有一些模式與 term的範例12。表格的第三個欄位(標示為結果)

顯示是否模式符合該 term,且如果是的話,繫結的變數為何。看看這些範例,

確定你真的瞭解一切:

模式 term 結果

{X,abc} {123,abc} 成功 X 123

{X,Y,Z} {222,def,"cat"} 成功 X 222, Y def, Z "cat"

{X,Y} {333,ghi,"cat"} 失敗。值組具有不同形狀(結構)

X true 成功 X true

{X,Y,X} {{abc,12},42,{abc,12}} 成功 X {abc,12}, Y 42

{X,Y,X} {{abc,12},42,true} 失敗。X無法同時為{abc,12}與 true

[H|T] [1,2,3,4,5] 成功 H 1, T [2,3,4,5]

[H|T] "cat" 成功 H 99, T "at"

[A,B,C|T] [a,b,c,d,e,f] 成功 A a, B b, C c, T [d,e,f]

如果上面的範例有你不確定者,試著在 Shell輸入「模式 = term」表示式,

看看會發生什麼事。

比方說:

1> {X, abc} = {123, abc}.

{123,abc}.

2> X.

123

12 term是指 Erlang資料結構。

33 Chapter 2 開始學習

3> f().

ok

4> {X,Y,Z} = {222,def,"cat"}.

{222,def,"cat"}.

5> X.

222

6> Y.

def

...

注意:f()用來告訴 Shell,忘掉它所具有的任何繫結。在此命令之後,所有

的變數都會變成未繫結,所以第 4行的 X和第 1、2行的 X沒有關係了。

現在我們已經習慣基本的資料型別,且具有單一指定與模式比對的知識,所

以我們可以加快步伐,看看如何定義函數與模組,這是下面兩章的內容。

top related