檢視原始碼 列表處理

建立列表

列表只能從尾端開始建立,並在開頭附加列表元素。如果您使用 ++ 運算子,如下所示,則會建立一個新列表,它是 List1 中元素的副本,後跟 List2

List1 ++ List2

看看 lists:append/2++ 在純 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]).

這裡建立了多個列表。在每個迭代步驟中,都會建立一個比前一個列表長一個元素的新列表。

為了避免在每次迭代中複製結果,請以相反的順序建立列表,並在完成後反轉列表

應該這樣做

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]).

列表推導式

列表推導式

[Expr(E) || E <- List]

基本上會轉換為本機函式

'lc^0'([E|Tail], Expr) ->
    [Expr(E)|'lc^0'(Tail, Expr)];
'lc^0'([], _Expr) -> [].

如果列表推導式的結果明顯地不會被使用,則不會建構列表。例如,在此程式碼中

[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) -> [].

編譯器也明白,分配給 _ 意味著該值將不會被使用。因此,以下範例中的程式碼也將被最佳化

_ = [io:put_chars(E) || E <- List],
ok.

深層和扁平列表

lists:flatten/1 會建立一個全新的列表。因此,它很耗費資源,甚至比 ++ 運算子(它複製其左引數,但不複製其右引數)更耗費資源。

在以下情況下,沒有必要呼叫 lists:flatten/1

  • 當將資料傳送到埠時。埠可以理解深層列表,因此沒有理由在將列表傳送到埠之前將其展平。
  • 當呼叫接受深層列表的 BIF 時,例如 list_to_binary/1iolist_to_binary/1
  • 當您知道您的列表只有一層深度時。請改用 lists:append/1

範例

應該這樣做

port_command(Port, DeepList)

請勿這樣做

port_command(Port, lists:flatten(DeepList))

將以零結尾的字串傳送到埠的常見方法如下

請勿這樣做

TerminatedStr = String ++ [0],
port_command(Port, TerminatedStr)

改為

應該這樣做

TerminatedStr = [String, 0],
port_command(Port, TerminatedStr)

應該這樣做

1> lists:append([[1], [2], [3]]).
[1,2,3]

請勿這樣做

1> lists:flatten([[1], [2], [3]]).
[1,2,3]

遞迴列表函式

有兩種基本方法可以編寫一個遍歷列表並產生新列表的函式。

第一種方法是編寫一個主體遞迴函式

%% Add 42 to each integer in the list.
add_42_body([H|T]) ->
    [H + 42 | add_42_body(T)];
add_42_body([]) ->
    [].

第二種方法是編寫一個尾部遞迴函式

%% Add 42 to each integer in the list.
add_42_tail(List) ->
    add_42_tail(List, []).

add_42_tail([H|T], Acc) ->
    add_42_tail(T, [H + 42 | Acc]);
add_42_tail([], Acc) ->
    lists:reverse(Acc).

在早期版本的 Erlang 中,尾部遞迴函式通常會更有效率。在現代版本的 Erlang 中,主體遞迴列表函式和最後反轉列表的尾部遞迴函式之間的效能通常沒有太大差異。因此,專注於編寫優美的程式碼,而忘記您的列表函式的效能。在程式碼的時效性關鍵部分,請在重寫程式碼之前進行測量

有關尾部和主體遞迴的深入討論,請參閱 Erlang 的尾部遞迴不是萬靈丹

注意

本節是關於建構列表的列表函式。不建構列表的尾部遞迴函式以恆定空間執行,而相應的主體遞迴函式使用與列表長度成比例的堆疊空間。

例如,對整數列表求和的函式,應如下編寫

請勿這樣做

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

改為

應該這樣做

sum(L) -> sum(L, 0).

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