原文鏈接:http://www.erlang.org/doc/efficiency_guide/listHandling.html
水平有限,錯誤之處歡迎指正。
5 列表解析
5.1 創建一個列表
創建列表最好從最后開始,一個元素接一個元素地附加在前面。如果你用++操作符:
List1 ++ List2
會通過把List1拷貝一份附加在List2前面來創建一個新的列表。看一下lists:append/1或者++在Erlang里是如何實現的,我們可以清楚地看到第一個列表被拷貝。
append([H|T], Tail) -> [H|append(T, Tail)]; append([], Tail) -> Tail.
所以當遞歸或者創建列表時要注意的是,確保把元素放到列表的前面,以便你創建列表時,隨着列表的生成,不會有成百上千的拷貝。
先讓我們看看不鼓勵的做法:
bad_fib(N) -> bad_fib(N, 0, 1, []). bad_fib(0, _Current, _Next, Fibs) -> Fibs; bad_fib(N, Current, Next, Fibs) -> bad_fib(N - 1, Next, Current + Next, Fibs ++ [Current]).
這里我不是創建了一個列表,每次迭代我們創建了一個新的列表,只比上一個多一個元素。
避免每次迭代都要拷貝結果,我們必須反序創建列表,在最后反轉之:
DO
tail_recursive_fib(N) -> tail_recursive_fib(N, 0, 1, []). tail_recursive_fib(0, _Current, _Next, Fibs) -> lists:reverse(Fibs); tail_recursive_fib(N, Current, Next, Fibs) -> tail_recursive_fib(N - 1, Next, Current + Next, [Current|Fibs]).
5.2 列表解析
列表解析依然被誤認為很慢。它們過去是用funs實現的,funs之前很慢。
在當前Erlang/OTP版本(包括R12B),列表解析
[Expr(E) || E <- List]
基本上被解析成一個本地函數
'lc^0'([E|Tail], Expr) -> [Expr(E)|'lc^0'(Tail, Expr)]; 'lc^0'([], _Expr) -> [].
在R12B中,如果列表解析的結果明顯地不會被用到,列表根本不會被構建。例如下面的代碼
[io:put_chars(E) || E <- List],
ok.
或者這樣的代碼
. . . case Var of ... -> [io:put_chars(E) || E <- List]; ... -> end, some_function(...), . . .
結果既不付給變量又不傳給另一個函數,也不是返回值,那么就沒有必要構建列表,編譯器會簡化這個列表解析成
'lc^0'([E|Tail], Expr) -> Expr(E), 'lc^0'(Tail, Expr); 'lc^0'([], _Expr) -> [].
5.3 嵌套和拉伸列表
lists:flatten/1創建一個全新的列表,因此,代價比較高,甚至比++還要高(++只會拷貝左邊的列表,右邊的不拷貝)。
以下情況可以避免使用lists:flatten/1:
a. 向端口發送數據。端口能夠處理嵌套列表,所以不須在發送前拉平列表。
b. 調用BIFs接收嵌套列表,例如list_to_binary/1或iolist_to_binary/1。
c. 當你的列表只有一層嵌套的時候,可以用lists:append/1。
Port example
DO
...
port_command(Port, DeepList)
...
DO NOT
...
port_command(Port, lists:flatten(DeepList))
...
通常會這樣向端口發送一個以0為結尾的字符串:
DO NOT
... TerminatedStr = String ++ [0], % String="foo" => [$f, $o, $o, 0] port_command(Port, TerminatedStr) ...
可以用這種方式來代替:
DO
... TerminatedStr = [String, 0], % String="foo" => [[$f, $o, $o], 0] port_command(Port, TerminatedStr) ...
Append example
DO
> lists:append([[1], [2], [3]]). [1,2,3] >
DO NOT
> lists:flatten([[1], [2], [3]]). [1,2,3] >
5.4 為什么不必擔心通過列表來遞歸的函數
在性能謬論那一章,下面這條謊言被揭穿:尾遞歸函數比遞歸函數快很多。
總的來說,在R12B里通常一個列表遞歸函數和尾遞歸加反轉沒有太大差別。因此,大多數情況下應該忽略列表函數的性能,重點關注代碼的整潔。只有在運行時間要求嚴格的那一小段代碼需要特殊照顧,並且在重寫它們之前一定要測試。
重要提示:這一節談論的列表函數都會構建列表。尾遞歸函數只需要恆定的空間運行,不用構建新的列表,而遞歸函數用到的棧空間和列表的長度成正比。例如,求和一個整數列表的函數不應該這樣寫
DO NOT
recursive_sum([H|T]) -> H+recursive_sum(T);
recursive_sum([]) -> 0.
而應該
DO
sum(L) -> sum(L, 0). sum([H|T], Sum) -> sum(T, Sum + H); sum([], Sum) -> Sum.
原創翻譯,歡迎任何形式的轉載,但請務必注明出處:http://www.cnblogs.com/liangjingyang