[Erlang-0006][OTP] 高效指南 -- 列表解析


原文鏈接: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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM