[Erlang0006][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

原文地址:https://www.cnblogs.com/liangjingyang/p/2592691.html