我正在教我自己的Erlang。一切顺利,直到我发现这个功能有问题。
-module(chapter).
-compile(export_all).
list_length([]) -> 0;
list_length([_|Xs]) -> 1+list_length([Xs]).
这是从教科书中取出的。当我使用OTP 17运行此代码时,它只是挂起,这意味着它只是如下所示。
1> c(chapter).
{ok,chapter}
2> chapter:list_length([]).
0
3> chapter:list_length([1,2]).
在查看任务管理器时,Erlang OTP使用200 Mb到330 Mb的内存。是什么导致了这一点。
答案 0 :(得分:4)
它不会终止,因为您在每种情况下都在创建一个新的非空列表:[Anything]
始终是非空列表,即使该列表包含空列表作为其唯一成员({{1是一个成员的非空列表。)
正确的列表终止如下:[[]]
。
所以考虑到这一点......
[ Something | [] ]
在大多数功能语言中,“正确列表”是缺点列表。查看the Wikipedia entry on "cons"和the Erlang documentation about lists,然后冥想您在示例代码中看到的列表操作示例。
备注强>
在运营商周围设置空白是一件好事;它会阻止你使用箭头和二进制语法运算符彼此相邻的混淆,同时避免一些其他歧义(并且它更容易阅读)。
史蒂夫指出,你注意到的内存爆炸是因为虽然你的函数是递归的,但它不是尾递归 - 也就是说,list_length([]) -> 0;
list_length([_|Xs]) -> 1 + list_length(Xs).
将待处理的工作留给完成后,必须在堆栈上留下引用。要为其添加1,必须完成1 + list_length(Xs)
的执行,返回一个值,并在这种情况下记住挂起值与列表中的成员一样多次。阅读Steve的答案,了解如何使用累加器值编写尾递归函数。
答案 1 :(得分:2)
由于OP正在学习Erlang,还要注意list_length/1
函数不适合tail call optimization,因为它的加法运算需要运行时递归调用函数,取其返回值,为其添加1,并返回结果。这需要堆栈空间,这意味着如果列表足够长,则可以用完堆栈。
请考虑采用这种方法:
list_length(L) -> list_length(L, 0).
list_length([], Acc) -> Acc;
list_length([_|Xs], Acc) -> list_length(Xs, Acc+1).
这种方法在Erlang代码中很常见,它在list_length/1
中创建一个累加器来保存长度值,将其初始化为0并将其传递给list_length/2
,后者执行递归。每次调用list_length/2
然后递增累加器,当列表为空时,list_length/2
的第一个子句返回累加器作为结果。但请注意,此处的加法操作在发生递归调用之前发生,这意味着调用是真正的尾调用,因此不需要额外的堆栈空间。
对于非初学者Erlang程序员,使用erlc -S
编译此模块的原始版本和修改版本并检查生成的Erlang汇编程序是有益的。对于原始版本,汇编程序包含allocate
对堆栈空间的调用,并使用call
进行递归调用,其中call
是正常函数调用的指令。但对于此修改版本,不会生成allocate
个调用,而是使用call
代替call_only
执行递归,而{{1}}针对尾调用进行了优化。