1. 程式人生 > 實用技巧 >第三章 順序型程式設計

第三章 順序型程式設計

3.1 模組

模組檔案以.erl為字尾名, 編譯後的檔案字尾名為.beam。
erl檔案示例

%% 模組名 與檔名相同
-module(geometry).
%% 函式定義 名稱及引數
-export([area/1]).
%% 函式實現 字句之間用 ; 分隔
area({rectangle, Width, Ht}) ->Width * Ht;
area({circle, R}) ->3.14159 * R * R.

執行示例

# 編譯
1> c(geometry).
{ok,geometry}

# 呼叫函式
2> geometry:area({rectangle, 10, 5}).
50
3> geometry:area({circle, 1.4}).
6.157516399999999

3.2 購物系統–進階篇

-module(shop1).
-export([total/1]).

%% 採用遞迴的方式將列表中的各項商品金額相加得到總金額
total([{What, N} | T]) ->
    shop:cost(What) * N + total(T);
total([]) ->0.

3.3 同名不同目的函式

-module(lib_misc).
-export([sum/1]).
-export([sum/2]).

%% 函式的目是指其引數數量
%% 同一模組中的同名不同目函式類似與java的過載
%% 但這裡的同名只是為了便於理解
%% 比如對列表元素求和, 實際是通過對列表的頭和尾進行遞迴求和實現的
sum(L) ->sum(L, 0).
sum([H|T], N) ->sum(T, H + N);
sum([], N) ->N.

3.4 fun、匿名函式、lambda

Erlang使用 fun 定義匿名函式, 效果同python、lisp中的lambda。

1> Hypot = fun(X, Y) -> math:sqrt(X*X + Y*Y) end.
#Fun<erl_eval.12.82930912>

2> Hypot(3, 4).
5.0

3> Double = fun(X) -> X*2 end.
#Fun<erl_eval.6.82930912>

4> Double(4).
8

3.4.1 以fun為引數的函式

1> L = [1, 2, 3, 4, 5, 6, 7, 8].
[1,2,3,4,5,6,7,8]

2> Even = fun(X) -> (X rem 2) =:= 0 end.
#Fun<erl_eval.6.82930912>

3> lists:map(Even, L).
[false,true,false,true,false,true,false,true]

4> lists:filter(Even, L).
[2,4,6,8]

3.4.2 返回fun的函式

1> Fruit = [apple, pear, orange].
[apple,pear,orange]

# 這裡返回了一個fun函式, 它可以判斷某個元素是否在指定的列表中 
2> MakeTest = fun(L) -> (fun(X) -> lists:member(X, L) end) end.
#Fun<erl_eval.6.82930912>

3> IsFruit = MakeTest(Fruit).
#Fun<erl_eval.6.82930912>

4> IsFruit(pear).
true

3.4.3 自定義抽象流程控制

%% 自定義for迴圈
%% 從 I 迴圈到 Max
%% 首先匹配 for(Max, Max, F)
%% 前兩個引數不相同將匹配 for(I, Max, F)
%% 然後將F(I) 與後續元素的計算結果拼接成列表
%% 當 I 迴圈到與 Max 相等時匹配for(Max, Max, F)
%% 如果兩個字句互換下順序, for(I, Max, F)匹配任意數字將導致死迴圈
for(Max, Max, F) ->[F(Max)];
for(I, Max, F) ->[F(I) | for(I+1, Max, F)].

3.5 簡單的列表處理

sum 和 map的實現

-module(mylists).
-export([sum/1]).
-export([map/2]).

sum([H|T]) ->H + sum(T);
sum([]) ->0. 

map(_, []) ->[];
map(F, [H|T]) ->[F(H) | map(F, T)].

使用sum和map改進total函式

-module(shop2).
-export([total/1]).
-import(mylists, [map/2, sum/1]).

total(L) ->
    sum(map(fun({What, N}) ->shop:cost(What) * N end, L)).

3.6 列表解析

一般形式為 [F(X) || X <- L]。
表示:由F(X)組成列表, 其中X取值於列表L。
map更為簡潔的實現

map(F, L) ->[F(X) || X <- L].

3.6.1 快速排序

%% 快速排序
%% 首先獲取列表頭元素
%% 然後使用列表解析將原列表的尾分成兩個列表, 其中一個列表為比頭元素小的集合, 另一個為比頭元素大的集合
%% 最後遞迴排序子列表
qsort([]) ->[];
qsort([Pivot | T]) ->
    qsort([X || X <- T, X < Pivot])
    ++ [Pivot] ++
    qsort([X || X <- T, X >= Pivot]).

示例:

1> L = [23, 6, 2, 9, 27, 400, 78].
[23,6,2,9,27,400,78]
2> lib_misc:qsort(L).
[2,6,9,23,27,78,400]

3.6.2 畢達哥拉斯三元組

%% 畢達哥拉斯三元組
%% 獲取在1到N之間的所有滿足 A^2 + B^2 = C^2的整數集合{A, B, C}
%% 根據要求羅列出條件由Erlang自動完成匹配
pythag(N) ->
    [ {A, B, C} ||
        A <- lists:seq(1, N),
        B <- lists:seq(1, N),
        C <- lists:seq(1, N),
        A+B+C =< N,
        A*A+B*B =:= C*C
    ].

示例:

1> lib_misc:pythag(16).
[{3,4,5},{4,3,5}]

3.6.3 變位詞

%% 變位詞
%% 依次取列表中的某個元素(H <- L)
%% 對剩餘的元素列表遞迴呼叫perms (perms(L--[H]))
%% 最後將兩者合併為同一個列表([H|T])
perms([]) ->[[]];
perms(L) ->[[H|T] || H <- L, T <- perms(L--[H])].

示例:

1> lib_misc:perms("1234").
["1234","1243","1324","1342","1423","1432","2134","2143",
 "2314","2341","2413","2431","3124","3142","3214","3241",
 "3412","3421","4123","4132","4213","4231","4312","4321"]

3.7 算術表示式

操作 描述
+X +X
-X -X
X*Y X*Y
X/Y X/Y
bnot X 按位取反
X div Y 整除取商
X rem Y 整除取餘
X band Y 按位取與
X+Y X+Y
X-Y X-Y
X bor Y 按位取或
X bxor Y 按位異或
X bsl N 按位左移
X bsr N 按位右移

3.8 斷言

斷言以”when”開頭, 可以在任何允許使用表示式的地方使用斷言。

3.8.1 斷言序列

使用分號分隔的斷言集合(G1;G2;…;GN)相當於 OR
使用逗號分隔的斷言集合(G1,G2,…GN)相當於 AND
斷言是模式匹配的一種擴充套件, 因此需要斷言表示式無副作用。

斷言謂詞

謂詞 含義
is_atom(X) 是否為原子
is_binary(X) 是否為二進位制資料
is_constant(X) 是否為常數
is_float(X) 是否為浮點數
is_integer(X) 是否為整數
is_number(X) 是否為整數或浮點數
is_list(X) 是否為列表
is_tuple(X) 是否為元組
is_reference(X) 是否為引用
is_pid(X) 是否為程序標示符
is_port(X) 是否為埠
is_record(X, Tag) 是否為標記為Tag的記錄
is_record(X, Tag, N) 是否為標記為Tag大小為N的記錄
is_function(X) 是否為函式
is_function(X, N) 是否為有N個引數的函式

斷言BIF(內建函式)

函式 含義
abs(X) 取絕對值
element(N, X) 取元組的第N個元素
float(X) 轉換為浮點數
hd(X) 取列表的頭部
length(X) 取列表的長度
node() 當前節點
node(X) 建立節點(pid, port, reference)
round(X) 四捨五入取整
self() 當前程序的標示符
size(X) 取元組或二進位制資料的大小
trunc(X) 擷取取整
tl(X) 取列表尾部

3.8.2 斷言樣例

max(X, Y) when is_integer(X), is_integer(Y), X > Y ->X;
max(X, Y) ->Y.

3.8.3 true斷言的使用

用在if表示式的末尾用於捕獲其它所有的情況。

3.8.4 過時的斷言函式

新版的Erlang測試函式的名稱大都是is_fun()的形式。

3.9 記錄

record用於使用一個名稱來對應元組中的元素。
record的定義存放在以.hrl為字尾名的檔案中。

%% 格式為 -record(Name, {key=value, ...}).
-record(todo, {status=reminder, who=joe, text}).

在shell中讀取記錄

1> rr("records.hrl").
[todo]

3.9.1 建立和更新記錄

# 建立todo型別的新記錄X, 值取預設 
2> X=#todo{}.
#todo{status = reminder,who = joe,text = undefined}

# 建立todo型別的新記錄X1, 自定義值 
3> X1=#todo{status=urgent, text="Fix errata in book"}.
#todo{status = urgent,who = joe,text = "Fix errata in book"}

# 複製記錄, 並修改其中一個key對應的值
4> X2=X1#todo{status=done}.
#todo{status = done,who = joe,text = "Fix errata in book"

3.9.2 從記錄中提取欄位值

# 採用模式匹配的方法提取欄位值 
5> #todo{who=W, text=Txt} = X2.
#todo{status = done,who = joe,text = "Fix errata in book"}
6> W.
joe
7> Txt.
"Fix errata in book"

# 或者使用"點語法"提取某個欄位值 
8> X2#todo.text.
"Fix errata in book"

3.9.3 在函式中對記錄進行模式匹配

使用斷言謂詞is_record(X, todo)來匹配X是否為todo型別的記錄。

3.9.4 記錄只是元組的偽裝

9> X2.
#todo{status = done,who = joe,text = "Fix errata in book"}

# 釋放對記錄的定義
10> rf(todo).
ok

# 將得到原有記錄中每個key對應的值所組成的元組
11> X2.
{todo,done,joe,"Fix errata in book"}

3.10 case/if表示式

case/if表示式是對模式匹配的補充。

3.10.1 case表示式

# 首先對Expression求值
# 依次對Pattern進行模式匹配
# 對匹配的分支, 將執行其相應的表示式 
case Expression of
    Pattern1 [when Guard1] -> Expr_seq1;
    Pattern2 [when Guard2] -> Expr_seq2;
    ...
end.

例項–模式匹配方式

%% filter的實現
%% filter函式依次對列表中的元素使用P函式求值
%% 根據P函式的結果(true或false)匹配filter1函式繼續執行
%% 在filter1中則繼續呼叫filter遞迴執行 
filter(P, [H|T]) ->filter1(P(H), H, P, T);
filter(P, []) ->[].

filter1(true, H, P, T) ->[H|filter(P, T)];
filter1(false, H, P, T) ->filter(P, T).

例項-case方式

%% 對P(H)求值, 不同的結果執行不同的表示式
filter(P, [H|T]) ->
    case P(H) of
        true   ->[H|filter(P, T)];
        false  ->filter(P, T)
    end;
filter(P, []) ->[].

3.10.2 if表示式

# 對Guard依次求值, 為true則執行後面的Expr_seq
# 如果都不匹配, 則最後應有一個原子true的斷言, 以保證至少有一個Expr_seq被執行。 
if 
    Guard1 -> Expr_seq1;
    Guard2 -> Expr_seq2;
    ...
    true   -> Expr_seq
end.

3.11 以自然順序建立列表

總是在列表頭部新增元素
從輸入列表的頭部提取元素, 加在一個輸出列表的頭部, 得到一個與輸入相反的列表
需要調整順序則呼叫高度優化的lists:reverse/1。
避免使用低效的 List ++ [H] 方式來生成自然順序的列表。

3.12 累加器

%% 將整數列表按奇偶分成兩個列表
%% 遍歷兩次, 一次取除2餘1, 一次取除2餘0
odds_and_evens(L) ->
    Odds = [X || X <- L, (X rem 2) =:= 1],
    Evens = [X || X <- L, (X rem 2) =:= 0],
    {Odds, Evens}.

%% 遍歷一次的版本
%% 依次對列表元素做除2操作, 根據不同的結果分別將其累加到Odds或Evens
odds_and_evens_acc(L) ->
    odds_and_evens_acc(L, [], []).
odds_and_evens_acc([H|T], Odds, Evens) ->
    case (H rem 2) of
        1 ->odds_and_evens_acc(T, [H|Odds], Evens);
        0 ->odds_and_evens_acc(T, Odds, [H|Evens])
    end;
odds_and_evens_acc([], Odds, Evens) ->{Odds, Evens}.