6  列表处理

6 列表处理

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

这里构建了多个列表。在每个迭代步骤中,都会创建一个新列表,该列表比之前的列表多一个元素。

为了避免在每次迭代中复制结果,请以相反的顺序构建列表,并在完成时反转列表

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

列表推导仍然有很慢的名声。它们过去是使用 Fun 实现的,而 Fun 过去很慢。

列表推导

[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], % String="foo" => [$f, $o, $o, 0]
      port_command(Port, TerminatedStr)
      ...

相反

      ...
      TerminatedStr = [String, 0], % String="foo" => [[$f, $o, $o], 0]
      port_command(Port, TerminatedStr) 
      ...

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

不要

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

在关于神话的部分,以下神话被揭露:尾递归函数比递归函数快得多

体递归列表函数和在最后反转列表的尾递归函数之间通常没有太大区别。因此,专注于编写优美的代码,而不要考虑列表函数的性能。在代码的关键部分(仅在那里),在重写代码之前**测量**。

注意

本节讨论的是**构造**列表的列表函数。不构造列表的尾递归函数在恒定空间内运行,而相应的体递归函数使用与列表长度成正比的堆栈空间。

例如,对整数列表求和的函数**不**应该按如下方式编写

不要

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.