# 第2章 串行編程
| 翻譯: | 連城 |
|-----|-----|
本章介紹用于編寫串行Erlang程序的概念。我們首先討論變量賦值的基本機制和如何實現控制流程。為此,我們要先了解一下**項式**、**模式**和**模式匹配**。
### 項式
Erlang中以下數據類型[[1]](#)被稱為**項式**:
>
> - **常量**類型
> - 數值
> - 整數,用于存儲自然數
> - 浮點數,用于存儲實數
> - 原子式
> - Pid(進程標識符process identifier的縮寫),用于存儲進程標識
> - 引用,用于存儲系統范圍內唯一的引用
> - **復合**數據類型
> - 元組,用于存儲固定數目的多個項式
> - 列表,用于存儲可變數目的多個項式
### 數值
以下實例都屬于數值:
~~~
123 -34567 12.345 -27.45e-05
~~~
整數精度與實現相關,但任何Erlang系統都應保證至少24位的整數精度。
$<Char>標記表示字符Char對應的ASCII值,如$A表示整數65。
不以10為基數的整數可寫作<Base>#<Value>,如16#ffff表示十進制整數65535。Base的取值范圍為2 .. 16。
浮點數以傳統方式書寫。
### 原子式
原子式是有名稱的常量。例如在某個用于日歷計算的程序中可使用monday、tuesday等等表示一星期中的各天。原子式用于增強程序的可讀性。
一些原子式實例:
~~~
friday unquoted_atoms_cannot_contain_blanks
'A quoted atom which contains several blanks'
'hello \n my friend'
~~~
原子式以小寫字母(a..z)開頭,以非字母數字字符結尾——否則就必須用引號括起來。
通過將原子式以引號括起來,原子式中便可以出現任意字符。原子式總是以可被 Erlang 讀取程序讀入的格式輸出。原子式引號內的字符遵循如下規范:
| 字符 | 含義 |
|-----|-----|
| \b | 退格符 |
| \d | 刪除符 |
| \e | 轉義符(ESC) |
| \f | 換頁符 |
| \n | 換行符 |
| \r | 回車符 |
| \t | 制表符 |
| \v | 垂直制表符 |
| \\ | 反斜線 |
| \^A..\^Z | control A到control Z(即0 .. 26) |
| \' | 單引號 |
| \" | 雙引號 |
| \OOO | 使用八進制格式OOO表示的字符 |
在引號括起來的原子式中如果包含字符序列\C,其中C的ASCII值小于32,則表示\C的這部分源碼被忽略(這樣我們在編程時就可以使用一個反斜線加換行符來將長原子式分隔為幾行)。
### 元組
以花括號包圍的一系列以逗號分隔的項式稱為**元組**。元組用于存儲固定數目個項式。它們與傳統編程語言中的**結構**或**記錄**類似。
元組{E1,E2,...,En},其中n大于0,稱為**大小**為n的元組。元組中的單個項式稱為**元素**。
以下是一些元組實例:
~~~
{a, 12, 'hello'}
{1, 2, {3, 4}, {a, {b, c}}}
{}
~~~
### 列表
以方括號包圍的一系列以逗號分隔的項式成為**列表**。列表用于存儲可變數目個項式。
對于列表[E1,E2,...En],其中n >= 0 ,稱其長度為n。
以下是一些元組實例:
~~~
[1, abc, [12], 'foo bar']
[]
[a,b,c]
"abcd"
~~~
被我們稱之為字符串的"..."標記,實際上是引號中各個字符組成的列表的ASCII簡寫形式。因此"abc"對應于[97,98,99]。在原子式中使用的轉義規則在字符串中通用。
在對列表進行處理時,往往需要一種方便的手段來引用列表的第一個元素以及除掉第一個元素以外列表的剩余部分。方便起見,我們將列表的第一個元素稱為**頭部**,將剩余部分稱為 *尾部* 。
我們使用[E1,E2,E3,...,En|Variable]來標記一個前n個元素分別為E1,E2,E3,...,En而剩余部分記為Variable的列表。
注意“|”之后的項式不一定要是列表,它可以是任意一個合法的Erlang項式。最后一個尾部為項式[]的列表稱為**真**列表或**格式良好**的列表——大多數(盡管不是全部)Erlang程序都是被編寫來處理格式良好的列表的。
### 模式匹配
模式與項式有著相同的結構,但它們還可以包含變量。變量名都以大寫字母開頭。
模式示例:
~~~
{A, a, 12, [12,34|{a}]}
{A, B, 23}
{x, {X_1}, 12, My_cats_age}
[]
~~~
以上的A、B、X_1和My_cats_age都是變量。
模式匹配為變量賦值提供了基本的機制。被賦值后,變量便**被綁定**——否則便是**未綁定**變量。給變量賦值的動作稱作“綁定”。變量一旦被綁定便不可更改。這種變量屬性被稱為**一次性綁定**或**單次賦值**。這種屬性與傳統命令式語言的**破壞性賦值**[[2]](#)相反。
如果一個**模式**與一個**項式**在結構上同構,且在模式中任一位置出現的原子數據類型也都在項式的相應位置上出現,則稱他們它們相互**匹配**。如果模式中包含未綁定變量,則該變量在匹配過程中將被綁定到項式中相應的元素。如果在模式中**相同的**變量多次出現,則項式中對應位置的元素也必須相同。
模式匹配在以下情況下發生:
- 計算形如Lhs=Rhs的表達式時
- 調用函數時
- 在case和receive原語中對指定模式進行匹配時
### Pattern=Expression
表達式Pattern=Expression將致使Expression被求值,并將其結果與Pattern進行匹配。匹配要么成功要么失敗。若匹配成功則Pattern中的所有(未綁定)變量都將被綁定。
以下我們將假設模式匹配總是**成功**。對**失敗**的處理將在第??章詳細討論。
示例:
~~~
{A, B} = {12, apple}
~~~
匹配成功后建立綁定關系A→12[[3]](#)和B→apple。
~~~
{C, [Head|Tail]} = {{222, man}, [a,b,c]}
~~~
匹配成功后建立綁定關系C→{222,man}、Head→a和Tail→[b,c]。
~~~
[{person, Name, Age, _}|T] =
[{person, fred, 22, male},
{person, susan, 19, female}, ...]
~~~
匹配成功后建立綁定關系T→[{person,susan,19,female},...]}、Name→fred和Age→22。在最后一個例子中我們利用了寫作“_”的**匿名**變量——在語法上需要一個變量出現,但我們又不關心該變量的值的時候便可以使用匿名變量。
當一個變量在一個模式中多次出現時,只有被匹配的對應元素的值都相同時匹配才會成功。因此,舉例來說,{A,foo,A}={123,foo,123}將成功匹配,并將A綁定到123,然而{A,foo,A}={123,foo,abc}就會失敗,因為我們不能將A同時綁定到123**和**abc。
“=”是一個右結合的中綴運算符。因此A=B=C=D將被解析為A=(B=(C=D))。這種用法可能只有在{A,B}=X=...這樣的構造中才有用,這時我們可以同時獲悉表達式的值及其組成部分。表達式Lhs=Rhs的值被定義為Rhs。
### 函數調用中的模式匹配
Erlang通過模式匹配來提供選擇和控制流程。例如,程序2.1定義了一個函數classify_day/1,當調用參數為saturday或sunday時返回weekEnd,否則返回weekDay 。
程序 2.1
~~~
-module(dates).
-export([classify_day/1]).
classify_day(saturday) -> weekEnd;
classify_day(sunday) -> weekEnd;
classify_day(_) -> weekDay.
~~~
進行函數求值時,會將函數的參數與函數定義中出現的模式一一進行匹配。一旦發現一個成功的匹配,“->”之后的符號便被求值,因此:
~~~
> dates:classify_day(saturday).
weekEnd
> dates:classify_day(friday).
weekDay
~~~
如果所有的子句都不匹配,則函數調用**失敗**(失敗將引發第??章描述的錯誤捕捉機制)。
當執行流程進入一個函數的某個子句時,描述該子句的模式所包含的變量將被綁定。因此,舉例來說,對程序1.3的math3:area({square,5})進行求值將致使變量Side被綁定到5。
### 表達式求值
表達式具備與模式相同的語法,同時表達式還可以包含函數調用或傳統的中序算術表達式。函數調用的寫法很傳統,如:area:triangle(A,B,C)便代表以參數A、B和C調用函數area:triangle。
Erlang 表達式的求值機制如下。
對項式求值得到其本身:
~~~
> 222.
222
> abc.
abc
> 3.1415926.
3.14159
> {a,12,[b,c|d]}.
{a,12,[b,c|d]}
> {{},[{}],{a,45,'hello world'}}.
{{},[{}],{a,45,'hello world'}}
~~~
浮點數的輸出格式可能與它們的輸入格式不完全一致。當表達式與項式同構且表達式中的函數調用都已求值完畢時,表達式將被求值為項式。應用一個函數時其參數首先被求值。
求值過程可以被認為是一個將表達式歸結為基礎項式的函數:
~~~
ε(X) when Constant(X)→X
ε({t1,t2,...,tn})→{ε(t1),ε(t2),...,ε(tn)}
ε([t1,t2,...,tn])→[ε(t1),ε(t2),...,ε(tn)]
ε(functionName(t1,t2,...,tn)→
APPLY(functionName,[ε(t1),ε(t2),...,ε(tn)])
~~~
其中APPLY表示一個將參數應用到函數的函數。
### 函數求值
函數調用的寫法如以下實例所示:
~~~
> length([a,b,c]).
3
> lists:append([a,b], [1,2,3]).
[a,b,1,2,3]
> math:pi().
3.14159
~~~
帶冒號形式的函數將在和模塊相關的章節中解釋。調用沒有參數的函數必須加上一對空的小括號(以此與原子式相區別)。
### 求值順序
函數參數的求值順序是不確定的。例如,f({a},b(),g(a,h(b),{f,X}))表示一個函數調用。對函數f的調用有三個參數:{a}、b()和g(a,h(b),{f,X})。第一個參數是一個只包含一個原子項a的元組。第二個參數是一個函數調用b()。第三個參數是函數調用g(a,h(b),{f,X})。在對f/3求值時,對b/0和g/3的求值順序是不確定的,不過h(b)在g/3被求值。對b()和h(b)的求值順序也是不確定的。
在對形如[f(a),g(b),h(k)]的表達式進行求值時,f(a)、g(b)和h(k)的求值順序是不確定的。
如果f(a)、g(b)和h(k)的求值過程沒有副作用(即不發送消息、不創建進程等等),則[f(a),g(b),h(k)]的**值**與求值順序無關[[4]](#)。這種屬性叫作**引用透明性**[[5]](#)。
### 應用
BIF apply(Mod,Func,ArgList)和apply({Mod,Func},ArgList)用于將模塊Mod中的函數Func應用到參數列表ArgList。
~~~
> apply(dates, classify_day, [monday]).
weekDay
> apply(math, sqrt, [4]).
2.0
> apply({erlang, atom_to_list}, [abc]).
[97,98,99]
~~~
使用apply對BIF進行求值時,可以使用erlang作為模塊名。
### 模塊系統
Erlang具備一套模塊系統以便我們將大型程序切分為一組模塊。每個模塊都有自己的名稱空間;這樣我們就可以在不同的模塊中自由地使用相同的函數名而不會有任何沖突。
模塊系統以對給定模塊中函數的可見性進行限制的方式來工作的。函數的調用方式取決于模塊名、函數名以及函數名是否在模塊的導入或導出聲明中出現。
程序 2.2
~~~
-module(lists1).
-export([reverse/1]).
reverse(L) ->
reverse(L, []).
reverse([H|T], L) ->
reverse(T, [H|L]);
reverse([], L) ->
L.
~~~
程序2.2定義了一個顛倒列表元素順序的函數reverse/1。reverse/1是該模塊中**唯一**可以從該模塊之外被調用的函數。需要從模塊外部調用的函數必須出現在模塊的導出聲明中。
該模塊中定義的其他函數,reverse/2,僅可供模塊**內部**使用。注意reverse/1和reverse/2是完全不同的函數。在Erlang中,名字相同但參數數目不同的兩個函數是完全不同的函數。
### 模塊間調用
從其他模塊中調用函數的方法有兩種:
程序 2.3
~~~
-module(sort1).
-export([reverse_sort/1, sort/1]).
reverse_sort(L) ->
lists1:reverse(sort(L)).
sort(L) ->
lists:sort(L).
~~~
reverse/1以**完全限定名稱**被調用。
你還可以借助import聲明使用**隱式限定函數名**,如程序2.4所示。
程序 2.4
~~~
-module(sort2).
-import(lists1, [reverse/1]).
-export([reverse_sort/1, sort/1]).
reverse_sort(L) ->
reverse(sort(L)).
sort(L) ->
lists:sort(L).
~~~
兩種形式都是為了解決二義性。比如,當兩個不同的模塊導出了重名的函數,則必須顯式限定函數名。
### 函數定義
以下章節更詳細地描述了Erlang函數的語法。首先我來給函數的各個語法元素命名。接著將詳細描述這些元素。
### 術語
考慮以下模塊:[[*]](#)
程序 2.5
~~~
-module(lists2).
-export([flat_length/1]).
%% flat_length(List)
%% Calculate the length of a list of lists.
flat_length(List) ->
flat_length(List, 0).
flat_length([H|T], N) when list(H) ->
flat_length(H, flat_length(T, N));
flat_length([H|T], N) ->
flat_length(T, N + 1);
flat_length([], N) ->
N.
~~~
以“%”打頭的是注釋。注釋可以從一行的任意位置開始,一直持續到行末。
第1行包含**模塊**聲明。該行必須出現在任何其他聲明或代碼之前。
第1行和第3行開頭的“-”稱為**屬性前綴**。module(list2)便是屬性的一個例子。
第2、第4等行是空行——連續的單個或多個空白符、空行、制表符、換行符等,都被當作單個空白符處理。
第3行聲明了一個具有一個參數的函數flag_length,該行意味著該函數存在于模塊中并會被從模塊中導出。
第5、6行是注釋。
第8、9行包含了函數flat_length/1的定義。它由單個**子句**組成。
表達式flat_length(List)稱為子句的**頭部**。“->”之后的部分為子句的**主體**。
第11至16行函數flat_length/2的定義——該函數包含三個子句;子句間以分號“;”分隔,在最后的結尾處以“.”結尾。
第11行中flat_length/2的第一個參數為列表[H|T]。H表示列表的**頭部**,T代表列表的**尾部**。在關鍵字when和箭頭“->”之間的表達式list(H)稱作保護式。只有在參數與函數頭部的模式相匹配且保護式斷言成立時,函數體才會被求值。
flat_length/2的第一個子句稱為**保護子句**;其他的子句稱為**無保護子句**。
flat_length/2是一個**局部函數**——即不可從模塊外部被調用(因為它沒有出現在export屬性中)。
模塊lists2包含了函數flat_length/1**和**flat_length/2的定義。它們代表**兩個完全不同的函數**——這與C或Pascal等語言不通,在這些語言中一個函數名只能出現一次,且只能有**固定**個數的參數。
### 子句
每個函數都由一組**子句**組成。子句間以分號“;”分隔。每個子句都包含一個子句頭部、一個可選的保護式和子句主體。下面將詳細解釋。
### 子句頭部
子句的頭部包含一個函數名和一組以逗號分隔的參數。每個參數都是一個合法的模式。
當函數調用發生時,將會按順序對函數定義中的子句頭部依次進行匹配。
### 子句保護式
保護式是子句被選中前必須要滿足的條件。
保護式可以是一個簡單的斷言或是一組由逗號分隔的簡單斷言。一個簡單斷言可以是一個算數比較、項式比較,或是一個系統預定義的斷言函數。保護式可以看作是模式匹配的一種擴展。用戶自定義的函數不能用在保護式內。
對保護式求值時所有的斷言都將被求值。若所有斷言都為真,則保護式成立,否則就失敗。保護式中各個斷言的求值順序是不確定的。
如果保護式成立,則會對子句的主體進行求值。如果保護式失敗,則嘗試下一個候選子句。
一旦子句的頭部和保護式都匹配成功,系統將**指定**這條子句并對其主體求值。
我們可以寫一個保護式版本的factorial。
~~~
factorial(N) when N == 0 -> 1;
factorial(N) when N > 0 -> N * factorial(N - 1).
~~~
注意對于以上示例,我們可以調換子句的順序,即:
~~~
factorial(N) when N > 0 -> N * factorial(N - 1);
factorial(N) when N == 0 -> 1.
~~~
在這個示例中子句首部模式與保護式的組合可以唯一確定一個正確的子句。
### 保護式斷言
保護式斷言的完整集合如下:
| 保護式 | 成立條件 |
|-----|-----|
| atom(X) | X是一個原子式 |
| constant(X) | X不是列表或元組 |
| float(X) | X是一個浮點數 |
| integer(X) | X是一個整數 |
| list(X) | X是一個列表或 [] |
| number | X是一個整數或浮點數 |
| pid(X) | X是一個進程標識符 |
| port(X) | X是一個端口 |
| reference(X) | X是一個引用 |
| tuple(X) | X是一個元組 |
| binary(X) | X是一段二進制數據 |
另外,一些BIF和算術表達式的組合也可以作為保護式。它們是:
~~~
element/2, float/1, hd/1, length/1, round/1, self/0, size/1
trunc/1, tl/1, abs/1, node/1, node/0, nodes/0
~~~
### 項式比較
可以出現在保護式中的項式比較運算符如下:
| 運算符 | 描述 | 類型 |
|-----|-----|-----|
| X>Y | X大于Y | coerce |
| X<Y | X小于Y | coerce |
| X=<Y | X小于或等于Y | coerce |
| X>=Y | X大于或等于Y | coerce |
| X==Y | X等于Y | coerce |
| X/=Y | X不等于Y | coerce |
| X=:=Y | X等于Y | exact |
| X=/=Y | X不等于Y | exact |
比較運算符工作機制如下:首先對運算符兩邊求值(如,在表達式兩邊存在算術表達式或包含BIF保護式函數時);然后再進行比較。
為了進行比較,定義如下的偏序關系:
~~~
number < atom < reference < port < pid < tuple < list
~~~
元組首先按大小排序,然后再按元素排序。列表的比較順序是先頭部,后尾部。
如果比較運算符的兩個參數都是數值類型且運算符為*coerce*型,則如果一個參數是integer另一個是float,那么integer將被轉換為float再進行比較。
exact類型的運算符則不做這樣的轉換。
因此5.0==1+4為真,而5.0=:=4+1為假。
保護函數子句示例:
~~~
foo(X, Y, Z) when integer(X), integer(Y), integer(Z), X == Y + Z ->
foo(X, Y, Z) when list(X), hd(X) == {Y, length(Z)} ->
foo(X, Y, Z) when {X, Y, size(Z)} == {a, 12, X} ->
foo(X) when list(X), hd(X) == c1, hd(tl(X)) == c2 ->
~~~
注意在保護式中不可引入新的變量。
### 子句主體
子句的主體有一個或多個有逗號分隔的表達式序列組成。序列中的表達式依次被求值。表達式序列的值被定義為序列中**最后一個**表達式的值。例如,factorial的第二個子句可以寫成:
~~~
factorial(N) when N > 0 ->
N1 = N - 1,
F1 = factorial(N1),
N * F1.
~~~
在對序列求值的過程中,表達式的求值結果要么與一個模式進行匹配,要么被直接丟棄。將函數主體拆分為序列的原因有這么幾條:
- 確保代碼的順序執行——函數主體中的表達式是依次求值的,而在嵌套的函數調用中的函數則可能以任意順序執行。
- 增強代碼可讀性——將函數寫成表達式序列可以令程序更清晰。
- (通過模式匹配)拆解函數的返回值。
- 重用函數調用的返回值。
對函數返回值的多次重用的示例如下:
~~~
good(X) ->
Temp = lic(X),
{cos(Temp), sin(Temp)}.
~~~
上面的寫法比下面這么寫要好:
~~~
bad(X) ->
{cos(lic(X)), sin(lic(X)}.
~~~
二者表達的是同一個含義。lic代表長而復雜的計算過程(Long and Involved Calculation),即那些計算代價高的函數。
### 原語
Erlang提供了元語case和if,這樣在子句中無需借助其他函數便可以直接進行條件求值。
### Case
case表達式允許在子句主體內部于多個選項中進行選擇,語法如下:
~~~
case Expr of
Pattern1 [when Guard1] -> Seq1;
Pattern2 [when Guard2] -> Seq2;
...
PatternN [when GuardN] -> SeqN
end
~~~
首先,對Expr求值,然后,Expr的值將依次與模式Pattern1、Pattern2……PatternN進行匹配,直到匹配成功。如果找到一個匹配并且(可選的)的保護式成立,則對應的調用序列將被求值。注意case保護式與函數保護式形式相同。case原語的值就是被選中的序列的值。
至少得有一個模式**必須**得以匹配——否則就會產生一個運行時錯誤并引發第??章中的錯誤處理機制。
舉個例子,比方說我們我有個函數allocate(Resource)用于分配某種資源Resource。假設這個函數只返回{yes,Address}或no。這樣,這個函數便可以放在一個case結構里:
~~~
...
case allocate(Resource) of
{yes,Address} when Address > 0, Address =< Max ->
Sequence 1 ... ;
no ->
Sequence 2 ...
end
...
~~~
在Sequence1...中,變量Address已經被綁定在了allocate/1的返回結果上。
為了避免匹配錯誤的發生,我們常常追加一個必會匹配的模式[[6]](#)作為case原語的最后一個分支:
~~~
case Fn of
...
_ ->
true
end
~~~
### If
if表達式的語法如下:
~~~
if
Guard1 ->
Sequence1 ;
Guard2 ->
Sequence2 ;
...
end
~~~
在這種情況下,保護式Guard1,...將被依次求值。如果一個保護式成立則對與之關聯的序列求值。該序列的求值結果便是if結構的結果。if保護式與函數保護式形式相同。與case相同,一個保護式都不成立的話將引發一個錯誤。如果需要,可以增加保護式斷言true作為垃圾箱:
~~~
if
...
true ->
true
end
~~~
### Case 和 if 使用示例
使用case和if我們可以以多種方式來編寫factorial。
最簡單的:
~~~
factorial(0) -> 1;
factorial(N) -> N * factorial(N - 1).
~~~
使用函數保護式:
~~~
factorial(0) -> 1;
factorial(N) when N > 0 -> N * factorial(N - 1).
~~~
使用if:
~~~
factorial(N) ->
if
N == 0 -> 1;
N > 0 -> N * factorial(N - 1)
end.
~~~
使用case:
~~~
factorial(N) ->
case N of
0 -> 1;
N when N > 0 ->
N * factorial(N - 1)
end.
~~~
使用變量保持臨時結果:
~~~
factorial(0) ->
1;
factorial(N) when N > 0 ->
N1 = N - 1,
F1 = factorial(N1),
N * F1.
~~~
以上所有定義都是正確且等價的[[7]](#)——如何進行選擇完全是個美學問題[[8]](#)。
### 算術表達式
算術表達式由以下運算符構成:
| 運算符 | 描述 | 類型 | 操作數類型 | 優先級 |
|-----|-----|-----|-----|-----|
| +X | +X | 單目 | 混合 | 1 |
| -X | -X | 單目 | 混合 | 1 |
| X*Y | X*Y | 雙目 | 混合 | 2 |
| X/Y | X/Y(浮點除法) | 雙目 | 混合 | 2 |
| XdivY | X整除Y | 雙目 | 整數 | 2 |
| XremY | X除以Y的余數 | 雙目 | 整數 | 2 |
| XbandY | X與Y的位與 | 雙目 | 整數 | 2 |
| X+Y | X+Y | 雙目 | 混合 | 3 |
| X-Y | X-Y | 雙目 | 混合 | 3 |
| XborY | X與Y位或 | 雙目 | 整數 | 3 |
| XbxorY | X與Y的位算數異或 | 雙目 | 整數 | 3 |
| XbslN | X算數左移N位 | 雙目 | 整數 | 3 |
| XbsrN | X右移N位 | 雙目 | 整數 | 3 |
**單目**運算符有一個參數,**雙目**運算符有兩個參數。**混合**意味著參數即可以是integer 也可以是float。單目運算符的返回值與其參數類型相同。
雙目混合運算符(即*、-、+)在參數都是integer時返回類型為integer的對象,在參數至少包含一個float時返回一個float。浮點除法運算符/總是返回一個float。
雙目整數運算符(即band、div、rem、bor、bxor、bsl、bsr)的參數必須是整數,其返回值也是整數。
求值順序取決于運算符的優先級:首先計算第1優先級的運算符,然后是第2優先級,以此類推。括號內的表達式優先求值。
優先級相同的運算符從左到右進行求值。比如:
~~~
A - B - C - D
~~~
其求值順序與下面的表達式一致:
~~~
(((A - B) - C) - D)
~~~
### 變量作用域
子句中變量的生存期從它首次被綁定處開始,到子句中對該變量的最后一個引用處結束。變量的綁定只會在模式匹配中發生;可以將之認作是一個變量產生過程。后續對變量的所有引用都是對變量的值的**使用**。**表達式中的變量必須是經過綁定的**。變量第一次出現時就被用在表達式中是非法的。比如:
<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1
2
3
4</pre><pre>f(X) ->
Y = g(X),
h(Y, X),
p(Y).
</pre></div></td></tr></table>
第1行中,定義了變量X(它在進入函數時被綁定)。第2行中,使用了X,定義了Y(首次出現)。第3行中,使用了X和Y,然后在第4行中使用了Y。
### if、case和receive的作用域規則
在if、case或receive原語中引入的變量會被隱式導出到原語主體之外。比方我們有:
~~~
f(X) ->
case g(X) of
true -> A = h(X);
false -> A = k(X)
end,
...
~~~
變量A在其被定義的case原語之后仍然有效。從if、case或receive原語中導出變量時應注意一些規則:
**在**if**、**case**或**receive**原語的不同分支中引入的變量集合必須相同,除非缺少的變量在原語外不再被引用。**
例如以下代碼:
~~~
f(X) ->
case g(X) of
true -> A = h(X), B = A + 7;
false -> B = 6
end,
h(A).
~~~
這段代碼就是非法的。因為在對true分支求值時定義了變量A和B,而在對false分支求值時只定義了B。在case原語之后,又在調用h(A)中引用了A——如果是fase分支被求值,則A尚未被定義。注意如果調用的不是h(A)而是h(B)則這段代碼就是合法的,因為B在case原語的兩個分支中都有定義。
腳注
| [[1]](#) | 附錄A給出了Erlang的形式語法。 |
|-----|-----|
| [[2]](#) | 許多人認為破壞性賦值會導致難以理解和易錯的不清晰的程序。 |
|-----|-----|
| [[3]](#) | 標記Var→Value表示變量Var的值為Value。 |
|-----|-----|
| [[4]](#) | 假設所有函數調用都結束。 |
|-----|-----|
| [[5]](#) | 即是說函數的**值**與調用上下文無關。 |
|-----|-----|
| [[6]](#) | 有時被稱為垃圾箱。 |
|-----|-----|
| [[7]](#) | 好吧,**幾乎是**——想想看factorial(-1)? |
|-----|-----|
| [[8]](#) | 如果不知道選哪個,選最漂亮的那個! |
|-----|-----|
| [[*]](#) | 譯者注:list/1在較新版本的Erlang中已經不推薦使用,應使用is_list/1。感謝網友[孔雀翎](#)指出。 |
|-----|-----|