# 第1章 Erlang教程
| 翻譯: | 連城 |
|-----|-----|
### 串行編程
程序1.1用于計算整數的階乘:
程序1.1
~~~
-module(math1).
-export([factorial/1]).
factorial(0) -> 1;
factorial(N) -> N * factorial(N - 1).
~~~
函數可以通過*shell*程序進行交互式求值。 Shell會提示輸入一個表達式,并計算和輸出用戶輸入的任意表達式,例如:
~~~
> math1:factorial(6).
720
> math1:factorial(25).
15511210043330985984000000
~~~
以上的“>”代表 shell 提示符,該行的其他部分是用戶輸入的表達式。之后的行是表達式的求值結果。
factorial 的代碼如何被編譯并加載至 Erlang 系統中是一個**實現相關**的問題。 [[1]](#)
在我們的例子中,factorial函數定義了兩個子句:第一個子句描述了計算factorial(0)的規則,第二個是計算factorial(N)的規則。當使用某個參數對factorial進行求值時,兩個子句按照它們在模塊中出現的次序被依次掃描,直到其中一個與調用相匹配。當發現一個匹配子句時,符號->右邊的表達式將被求值,求值之前函數定義式中的變量將被代入右側的表達式。
所有的 Erlang 函數都從屬于某一特定**模塊**。最簡單的模塊包含一個模塊聲明、**導出**聲明,以及該模塊導出的各個函數的實現代碼。導出的函數可以從模塊**外部**被調用。其他函數只能在模塊內部使用。
程序1.2是該規則的一個示例。
程序1.2
~~~
-module(math2).
-export([double/1]).
double(X) ->
times(X, 2).
times(X, N) ->
X * N.
~~~
函數double/1可在模塊外被求值[[2]](#),times/2則只能在模塊內部使用,如:
~~~
> math2:double(10).
20
> math2:times(5, 2).
** undefined function: math2:times(5,2) **
~~~
在程序1.2中**模塊聲明**-module(math2)定義了該模塊的名稱,**導出屬性**-export([double/1])表示本模塊向外部導出具備一個參數的函數double。
函數調用可以嵌套:
~~~
> math2:double(math2:double(2)).
8
~~~
Erlang 中的選擇是通過模式匹配完成的。程序 1.3 給出一個示例:
程序1.3
~~~
-module(math3).
-export([area/1]).
area({square, Side}) ->
Side * Side;
area({rectangle, X, Y}) ->
X * Y;
area({circle, Radius}) ->
3.14159 * Radius * Radius;
area({triangle, A, B, C}) ->
S = (A + B + C)/2,
math:sqrt(S*(S-A)*(S-B)*(S-C)).
~~~
如我們所期望的,對math3:area({triangle,3,4,5})得到6.0000而math3:area({square,5})得到 25 。程序1.3 引入了幾個新概念:
> > **元組**——用于替代復雜數據結構。我們可以用以下 shell 會話進行演示:
> >
~~~
> Thing = {triangle, 6, 7, 8}.
{triangle, 6, 7, 8}
> math3:area(Thing).
20.3332
~~~
> 此處Thing被綁定到{triangle,6,7,8}——我們將Thing稱為**尺寸**為4的一個元組——它包含 4 個**元素**。第一個元素是**原子式**triangle,其余三個元素分別是整數6、7和8。
> **模式識別**——用于在一個函數中進行子句選擇。area/1被定義為包含4個**子句**。以math3:area({circle,10})為例, 系統會嘗試在area/1定義的子句中找出一個與{circle,10}相符的匹配,之后將函數定義**頭部**中出現的自由變量Radius**綁定**到調用中提供的值(在這個例子中是10)。
> **序列**和**臨時變量**——這二者是在area/1定義的最后一個子句中出現的。最后一個子句的**主體**是由兩條以逗號分隔的語句組成的序列;序列中的語句將**依次**求值。函數子句的值被定義為語句序列中的**最后**一個語句的值。在序列中的第一個語句中,我們引入了一個臨時變量S。
### 數據類型
Erlang 提供了以下數據類型:
> > **常量**數據類型——無法再被分割為更多原始類型的類型:
> - **數值**——如:123、-789、3.14159、7.8e12、-1.2e-45。數值可進一步分為**整數**和**浮點數**。
> - **Atom**——如:abc、'Anatomwithspaces'、monday、green、hello_word。它們都只是一些命名常量。
> **復合**數據類型——用于組合其他數據類型。復合數據類型分為兩種:
> - **元組**——如:{a,12,b}、{}、{1,2,3}、{a,b,c,d,e}。元組用于存儲固定數量的元素,并被寫作以花括號包圍的元素序列。元組類似于傳統編程語言中的記錄或結構。
> - **列表**——如:[]、[a,b,12]、[22]、[a,'hellofriend']。列表用于存儲可變數量的元素,并被寫作以方括號包圍的元素序列。
元組和列表的成員本身可以是任意的 Erlang 數據元素——這使得我們可以創建任意復雜的數據結構。
在 Erlang 中可使用**變量**存儲各種類型的值。變量總是以大寫字母開頭,例如,以下代碼片段:
~~~
X = {book, preface, acknowledgements, contents,
{chapters, [
{chapter, 1, 'An Erlang Tutorial'},
{chapter, 2, ...}
]
}},
~~~
創建了一個復雜的數據結構并將其存于變量X中。
### 模式識別
模式識別被用于變量賦值和程序流程控制。Erlang是一種**單性賦值**語言,即一個變量一旦被賦值,就再也不可改變。
模式識別用于將模式與項式進行匹配。如果一個模式與項式具備相同的結構則匹配成功,并且模式中的所有變量將被綁定到項式中相應位置上出現的數據結構。
### 函數調用中的模式識別
程序1.4定義了在攝氏、華氏和列式溫標間進行溫度轉換的函數convert。convert的第一個參數是一個包含了溫標和要被轉換的溫度值,第二個參數是目標溫標。
程序1.4
~~~
-module(temp).
-export([convert/2]).
convert({fahrenheit, Temp}, celsius) ->
{celsius, 5 * (Temp - 32) / 9};
convert({celsius, Temp}, fahrenheit) ->
{farenheit, 32 + Temp * 9 / 5};
convert({reaumur, Temp}, celsius) ->
{celsius, 10 * Temp / 8};
convert({celsius, Temp}, reaumur) ->
{reaumur, 8 * Temp / 10};
convert({X, _}, Y) ->
{cannot,convert,X,to,Y}.
~~~
對convert進行求值時,函數調用中出現的參數(項式)與函數定義中的模式進行匹配。當找到一個匹配時,“->”右側的代碼便被求值,如:
~~~
> temp:convert({fahrenheit, 98.6}, celsius).
{celsius,37.0000}
> temp:convert({reaumur, 80}, celsius).
{celsius,100.000}
> temp:convert({reaumur, 80}, fahrenheit).
{cannot,convert,reaumur,to,fahrenheit}
~~~
### 匹配原語“=”
表達式Pattern=Expression致使Expression被求值并嘗試與\ Pattern 進行匹配。匹配過程要么成功要么失敗。一旦匹配成功,則Pattern中所有的變量都被綁定,例如:
~~~
> N = {12, banana}.
{12,banana}
> {A, B} = N.
{12,banana}
> A.
12
> B.
banana
~~~
匹配原語可用于從復雜數據結構中拆分元素。
~~~
> {A, B} = {[1,2,3], {x,y}}.
{[1,2,3],{x,y}}
>A.
[1,2,3]
>B.
{x,y}
> [a,X,b,Y] = [a,{hello, fred},b,1].
[a,{hello,fred},b,1]
> X.
{hello,fred}
> Y.
1
> {_,L,_} = {fred,{likes, [wine, women, song]},
{drinks, [whisky, beer]}}.
{fred,{likes,[wine,women,song]},{drinks,[whisky,beer]}}
> L.
{likes,[wine,women,song]}
~~~
下劃線(寫作“_”)代表特殊的**匿名**變量或**無所謂**變量。在語法要求需要一個變量但又不關心變量的取值時,它可用作占位符。
如果匹配成功,定義表達式Lhs=Rhs的取值為Rhs。這使得在單一表達式中使用多重匹配成為可能,例如:
~~~
{A, B} = {X, Y} = C = g{a, 12}
~~~
“=”是右結合操作符,因此A=B=C=D被解析為A=(B=(C=D))。
### 內置函數
有一些操作使用Erlang編程無法完成,或無法高效完成。例如,我們無法獲悉一個原子式的內部結構,或者是得到當前時間等等——這些都屬于語言范疇之外。因此Erlang提供了若干**內置函數**(built-in function, BIF)用于完成這些操作。
例如函數atom_to_list/1將一個原子式轉化為一個代表該原子式的(ASCII)整數列表,而函數date/0返回當前日期:[[*]](#)
~~~
> atom_to_list(abc).
[97,98,99]
> date().
{93,1,10}
~~~
BIF的完整列表參見附錄??。
### 并發
Erlang是一門**并發**編程語言——這意味著在Erlang中可直接對并行活動(進程)進行編程,并且其并行機制是由Erlang而不是宿主操作系統提供的。
為了對一組并行活動進行控制,Erlang提供了多進程原語:spawn用于啟動一個并行計算(稱為進程);send向一個進程發送一條消息;而receive從一個進程中接收一條消息。
spawn/3啟動一個并發進程并返回一個可用于向該進程發送消息或從該進程接收消息的標識符。
Pid!Msg語法用于消息發送。Pid是代表一個進程的身份的表達式或常量。Msg是要向Pid發送的消息。例如:
~~~
Pid ! {a, 12}
~~~
表示將消息{a,12}發送至以Pid為標識符的進程(Pid是**進程標識符process identifier**的縮寫)。在發送之前,消息中的所有參數都先被求值,因此:
~~~
foo(12) ! math3:area({square, 5})
~~~
表示對foo(12)求值(必須返回一個有效的進程標識符),并對math3:area({square,5})求值,然后將計算結果(即25)作為一條消息發送給進程。send原語兩側表達式的求值順序是不確定的。
receive原語用于接收消息。receive語法如下:
~~~
receive
Message1 ->
... ;
Message2 ->
... ;
...
end
~~~
這表示嘗試接收一個由Message1、Message2等模式之一描述的消息。對該原語進行求值的進程將被掛起,直至接收到一個與Message1、Message2等模式匹配的消息。一旦找到一個匹配,即對“->”右側的代碼求值。
接收到消息后,消息接收模式中的所有未綁定變量都被綁定。
receive的返回值是被匹配上的接收選項所對應的語句序列的求值結果。
我們可以簡單認為send發生一條消息而receive接收一條消息,然而更準確的描述則是send將一條消息**發送至一個進程的郵箱**,而receive**嘗試從當前進程的郵箱中取出一條消息**。
receive是有選擇性的,也就是說,它從等候接收進程關注的消息隊列中取走第一條與消息模式相匹配的消息。如果找不到與接收模式相匹配的消息,則進程繼續掛起直至下一條消息到來——未匹配的消息被保存用于后續處理。
### 一個echo進程
作為一個并發進程的簡單示例,我們創建一個*echo*進程用于原樣發回它所接收到的消息。我們假設進程A向echo進程發送消息{A,Msg},則echo進程向A發送一條包含Msg的新消息。如圖1.1所示。

圖1.1 一個echo進程
在程序1.5中echo:start()創建一個返回任何發送給它的消息的簡單進程。
程序 1.5
~~~
-module(echo).
-export([start/0, loop/0]).
start() ->
spawn(echo, loop, []).
loop() ->
receive
{From, Message} ->
From ! Message,
loop()
end.
~~~
spawn(echo,loop[])對echo:loop()所表示的函數相對于調用函數**并行**求值。因此,針對:
~~~
...
Id = echo:start(),
Id ! {self(), hello}
...
~~~
進行求值將會啟動一個并行進程并向該進程發送消息{self(),hello}——self()是用于獲取當前進程標識符的BIF。
腳注
| [[1]](#) | “實現相關”是指**如何**完成某個具體操作的細節是系統相關的,也不在本書的討論范疇之內。 |
|-----|-----|
| [[2]](#) | F/N標記表示具備N個參數的函數F。 |
|-----|-----|
| [[*]](#) | 譯者注:在較新版本的Erlang中,該示例的輸出為"abc"。當Erlang shell猜測出待打印的列表為字符串時,會嘗試以字符串形式輸出列表,參見[此處](http://www.erlang.org/pipermail/erlang-questions/2002-September/005624.html) [http://www.erlang.org/pipermail/erlang-questions/2002-September/005624.html]。感謝網友[孔雀翎](#)指出。 |
|-----|-----|