只是想直截了当。考虑一下Erlang代码的这个例子:
test() ->
receive
{From, whatever} ->
%% do something
test();
{From, somethingelse} ->
%% do something else
test();
end.
不是test()调用,只是goto?
我问这个是因为在C中我们了解到,如果你进行函数调用,返回位置总是放在堆栈上。我无法想象这里的Erlang一定是这种情况,因为这会导致堆栈溢出。
基本。我们有两种不同的调用函数的方法: goto和gosub。 goto只是将程序流引导到其他地方,gosub记得你来自哪里,所以你可以回来。
考虑到这种思维方式,我可以更容易地看看erlang的递归,因为如果我只读:test()作为goto,那么根本就没有问题。
因此我的问题:不是erlang只是使用goto而不是记住堆栈上的返回地址吗?
编辑:
只是为了澄清我的观点:
我知道goto可以在某些语言中用于跳转到处。但是,只是做suupose而不是做someFunction()你也可以这样做:转到someFunction() 在第一个例子中,流程返回,在第二个例子中,流程只在someFunction中继续,永远不会返回。
因此我们只能跳转到方法起点来限制正常的GOTO行为。
如果你这样看,那么erlang递归函数调用看起来像是一个goto。
(我认为这是一个函数调用,无法返回你来自的地方)。这正是erlang示例中正在发生的事情。
答案 0 :(得分:14)
尾部递归调用更多的是“返回并立即调用此其他函数”而不是goto,因为执行了内务处理。
解决您的最新要点:记录返回点只是在调用函数时执行的一项内务处理。返回点存储在堆栈帧中,其余部分必须分配和初始化(在正常调用中),包括局部变量和参数。使用尾递归,不需要分配新帧,并且不需要存储返回点(前一个值正常工作),但需要执行其余的内务处理。
当函数返回时,还需要执行内务处理,包括丢弃本地和参数(作为堆栈帧的一部分)并返回到调用点。在尾递归调用期间,当前函数的locals在调用new函数之前被丢弃,但是没有返回。
与线程如何允许比进程更轻量级的上下文切换一样,尾部调用允许更轻量级的函数调用,因为可以跳过一些内务处理。
Perl中的“goto &NAME
”语句更接近您的想法,但并不完全,因为它丢弃了本地人。为新调用的函数保留参数。
还有一个简单的区别:尾部调用只能跳转到函数入口点,而goto可以跳转到任何地方(某些语言限制goto的目标,例如C,其中goto不能跳到a功能)。
答案 1 :(得分:8)
你是对的,Erlang编译器会检测到它是一个尾递归调用,而不是在堆栈上继续运行,它会重用当前函数的堆栈空间。
此外,它还能够检测循环尾递归,例如
test() -> ..., test2().
test2() -> ..., test3().
test3() -> ..., test().
也将进行优化。
这种“不幸”的副作用是当你跟踪函数调用时,你将无法看到每次调用尾递归函数,而是查看入口和出口点。
答案 2 :(得分:4)
你在这里有两个问题。
首先,不,在这种情况下你没有超出堆栈的危险,因为对test()的这些调用都是tail-recursive。
第二,不,函数调用不是goto,它们是函数调用。 :)使goto有问题的是它绕过代码中的任何结构。你可以跳出陈述,跳进陈述,绕过作业......各种各样的蠢事。函数调用没有这个问题,因为它们有明显的控制流。
答案 3 :(得分:3)
我认为这里的区别在于“真正的”goto和在某些情况下看起来像是goto的东西。在某些特殊情况下,编译器可以检测到在调用另一个函数之前可以自由清理当前函数的堆栈。这是呼叫是函数中的最后一次调用。当然,不同之处在于,您可以将参数传递给新函数。
正如其他人所指出的,这种优化并不局限于递归调用,而是局限于所有最后调用。这用于编程FSM的“经典”方式。
答案 4 :(得分:3)
goto
if
goto
和while
goto
的原因与goto
相同。它是使用(道德等价的)goto
来实现的,但它没有直接向程序员揭示{{1}}的完全射击 - 自我潜在的潜力。
答案 5 :(得分:2)
事实上,根据Guy Steele的说法,这些递归函数是the ultimate GOTO。
答案 6 :(得分:2)
答案 7 :(得分:1)
在这种情况下,可以进行尾调用优化,因为我们不需要做更多工作或使用局部变量。因此编译器会将其转换为循环。
答案 8 :(得分:1)
(我认为这是一个函数调用,无法返回你来自的地方)。这正是erlang示例中正在发生的事情。
这不是Erlang中发生的事情,你可以回到你来自哪里。
调用是尾递归的,这意味着它是“一种”goto。在尝试理解或编写任何代码之前,请确保了解尾递归是什么。如果你是Erlang的新手,阅读Joe Armstrong的书可能并不是一个坏主意。
从概念上讲,如果您使用test()调用自己,则使用您传递的任何参数(在此示例中为none)调用函数的开头,但不会向堆栈添加任何其他参数。因此,所有变量都被丢弃,函数重新开始,但是没有将新的返回指针推送到堆栈上。所以它就像goto和传统命令式语言风格函数调用之间的混合,就像你在C或Java中所拥有的一样。但是从调用函数的第一次调用开始,堆栈上仍有一个条目。因此,当您最终通过返回值而不是执行另一个test()时退出,然后从堆栈中弹出该返回位置,并在您的调用函数中继续执行。