查看源码 列表处理

创建列表

列表只能从末尾开始构建,并在开头附加列表元素。 如果你使用 ++ 操作符,如下所示,将会创建一个新的列表,该列表是 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.