递归函数调用挂起,Erlang

时间:2015-04-25 14:32:47

标签: windows memory erlang erlang-shell

我正在教我自己的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的内存。是什么导致了这一点。

2 个答案:

答案 0 :(得分:4)

它不会终止,因为您在每种情况下都在创建一个新的非空列表:[Anything]始终是非空列表,即使该列表包含空列表作为其唯一成员({{1是一个成员的非空列表。)

正确的列表终止如下:[[]]

所以考虑到这一点......

[ Something | [] ]

在大多数功能语言中,“正确列表”是缺点列表。查看the Wikipedia entry on "cons"the Erlang documentation about lists,然后冥想您在示例代码中看到的列表操作示例。

备注

  1. 在运营商周围设置空白是一件好事;它会阻止你使用箭头和二进制语法运算符彼此相邻的混淆,同时避免一些其他歧义(并且它更容易阅读)。

  2. 史蒂夫指出,你注意到的内存爆炸是因为虽然你的函数是递归的,但它不是尾递归 - 也就是说,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}}针对尾调用进行了优化。