/*not the fanciest, it's just meant to
reenforce that I know what I'm doing.*/
function loopy(x){
if (x<10000000){
return function(){
return loopy(x+1)
}
}else{
return x;
}
};
function trampoline(foo){
while(foo && typeof foo === 'function'){
foo = foo();
}
return foo;
/*I've seen trampolines without this,
mine wouldn't return anything unless
I had it though. Just goes to show I
only half know what I'm doing.*/
};
alert(trampoline(loopy(0)));
我最大的问题是我不知道为什么这样有效。我的想法是在while循环中重新运行该函数,而不是使用递归循环。除了,从技术上讲,我的基本函数已经有一个递归循环。我没有运行基本的
loopy
函数,但正在其中运行该函数。是什么阻止foo = foo()
导致堆栈溢出? Q4312079q是否在技术上是变异的,还是我缺少某些东西?也许这只是必要的邪恶。或缺少某些语法。甚至还有一种方法可以理解它?还是只是某种技巧可以奏效?我本来可以遍历其他所有事物,但是这使我感到困惑。
#1 楼
您的大脑反抗loopy()
函数的原因是它的类型不一致:function loopy(x){
if (x<10000000){
return function(){ // On this line it returns a function...
// (This is not part of loopy(), this is the function we are returning.)
return loopy(x+1)
}
}else{
return x; // ...but on this line it returns an integer!
}
};
很多语言甚至都不允许您做类似的事情这个,或者至少需要更多的打字来解释这应该有什么意义。因为确实没有。函数和整数是完全不同的对象。
因此,让我们仔细检查一下while循环:
while(foo && typeof foo === 'function'){
foo = foo();
}
最初,
foo
等于至loopy(0)
。什么是loopy(0)
?好吧,它小于10000000,所以我们得到function(){return loopy(1)}
。这是一个真实的值,它是一个函数,因此循环继续进行。现在我们来看看
foo = foo()
。 foo()
与loopy(1)
相同。由于1仍小于10000000,将返回function(){return loopy(2)}
,然后将其分配给foo
。foo
仍然是一个函数,因此我们继续进行操作,直到最终foo等于function(){return loopy(10000000)}
为止。那是一个函数,所以我们再做一次foo = foo()
,但是这一次,当我们调用loopy(10000000)
时,x不小于10000000,所以我们只得到x即可。由于10000000也不是一个函数,因此也会结束while循环。评论
评论不作进一步讨论;此对话已移至聊天。
– yannis
16-10-12在9:41
它实际上只是一个求和类型。有时称为变体。动态语言相当容易地支持它们,因为每个值都被标记了,而更多的静态类型的语言将需要您指定函数返回一个变体。例如,在C ++或Haskell中很容易实现蹦床。
– GManNickG
16-10-12在20:54
@GManNickG:是的,这就是我的意思,“更多打字”。在C语言中,您必须声明一个联合,声明一个对联合进行标记的结构,在任一端对结构进行打包和解压缩,在任一端对结构进行打包和解压缩,并(大概)弄清楚谁拥有该结构所驻留的内存。 C ++的代码可能比C少,但从概念上讲,它并不比C复杂,并且比OP的Java脚本还要冗长。
–凯文
16-10-12在21:13
当然,我没有提出异议,我只是认为您对它的怪异或不合理的强调有点强。 :)
– GManNickG
16-10-12在21:18
#2 楼
凯文(Kevin)简洁地指出了此特定代码段是如何工作的(以及为什么它很难理解),但我想补充一些有关蹦床在一般工作中的工作方式的信息。每个函数调用都会向当前执行堆栈中添加一个堆栈框架。假设我们有一个打印数字倒数的函数:function countdown(n) {
if (n === 0) {
console.log("Blastoff!");
} else {
console.log("Launch in " + n);
countdown(n - 1);
}
}
如果调用
countdown(3)
,让我们分析没有TCO时调用堆栈的外观。> countdown(3);
// stack: countdown(3)
Launch in 3
// stack: countdown(3), countdown(2)
Launch in 2
// stack: countdown(3), countdown(2), countdown(1)
Launch in 1
// stack: countdown(3), countdown(2), countdown(1), countdown(0)
Blastoff!
// returns, stack: countdown(3), countdown(2), countdown(1)
// returns, stack: countdown(3), countdown(2)
// returns, stack: countdown(3)
// returns, stack is empty
使用TCO,对
countdown
的每个递归调用都位于尾部位置(除了返回调用结果之外,别无他法),因此不分配堆栈帧。如果没有TCO,则即使n
稍大,堆栈也会炸毁。通过在
countdown
函数周围插入包装器,油菜籽油可以解决此限制。然后,countdown
不执行递归调用,而是立即返回要调用的函数。下面是一个示例实现:function trampoline(firstHop) {
nextHop = firstHop();
while (nextHop) {
nextHop = nextHop()
}
}
function countdown(n) {
trampoline(() => countdownHop(n));
}
function countdownHop(n) {
if (n === 0) {
console.log("Blastoff!");
} else {
console.log("Launch in " + n);
return () => countdownHop(n-1);
}
}
为了更好地了解其工作原理,让我们看一下调用堆栈:
> countdown(3);
// stack: countdown(3)
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(3)
Launch in 3
// return next hop from countdownHop(3)
// stack: countdown(3), trampoline
// trampoline sees hop returned another hop function, calls it
// stack: countdown(3), trampoline, countdownHop(2)
Launch in 2
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(1)
Launch in 1
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(0)
Blastoff!
// stack: countdown(3), trampoline
// stack: countdown(3)
// stack is empty
在每个步骤中,
countdownHop
函数都会放弃对接下来发生的事情的直接控制,而是返回一个调用来描述接下来要发生的事情。然后,蹦床函数会接受并调用它,然后再调用返回的任何函数,依此类推,直到没有“下一步”为止。之所以称其为蹦床,是因为每个递归调用与蹦床实现之间的控制流“反弹”,而不是直接重复执行该函数。通过放弃对谁进行递归调用的控制,蹦床功能可以确保堆栈不会太大。旁注:trampoline
的此实现为简单起见省略了返回值。要知道这是否是一个好主意可能很棘手。由于分配新封包的每个步骤都会降低性能。明智的优化可以使之可行,但您永远不会知道。 Trampolining在解决硬递归限制方面非常有用,例如,当语言实现设置最大调用堆栈大小时。
#3 楼
如果使用专用返回类型(而不是滥用函数)实现蹦床,可能会变得更容易理解:是什么原因阻止了
trampoline
导致堆栈溢出?它不再自称。相反,它返回一个结果(在我的实现中为
foo = foo()
),该结果传达是继续进行递归还是中断该循环。我想念什么吗?也许这只是必要的邪恶。是的,这正是循环的必要邪恶。一个人也可以写没有突变的
Result
,但是它又需要递归:class Result {}
// poor man's case classes
class Recurse extends Result {
constructor(a) { this.arg = a; }
}
class Return extends Result {
constructor(v) { this.value = v; }
}
function loopy(x) {
if (x<10000000)
return new Recurse(x+1);
else
return new Return(x);
}
function trampoline(fn, x) {
while (true) {
const res = fn(x);
if (res instanceof Recurse)
x = res.arg;
else if (res instanceof Return)
return res.value;
}
}
alert(trampoline(loopy, 0));
它仍然显示出蹦床功能甚至更好的想法。 />
践踏的要点是从想要使用递归的函数中提取尾递归调用,并将其抽象为返回值,然后仅在一个地方进行实际的递归-
foo = foo()
函数,然后可以对其进行优化在一个地方使用循环。评论
foo = foo()在修改局部状态的意义上是突变,但我通常会认为重新分配是因为您实际上并未在修改底层函数对象,而是将其替换为其返回的函数(或值)。
– JAB
16-10-11在21:21
@JAB是的,我并不是要暗示要更改foo包含的值,只修改变量。如果希望终止,while循环需要一些可变状态,在这种情况下,该状态为变量foo或x。
–贝尔吉
16-10-11在22:05
不久前,我在回答有关尾声优化,蹦床等问题的Stack Overflow问题时做了类似的事情。
–约书亚·泰勒(Joshua Taylor)
16-10-12在18:24
您的无突变版本已将fn的递归调用转换为蹦床的递归调用-我不确定这是否有所改善。
–迈克尔·安德森(Michael Anderson)
16-10-13在1:01
@MichaelAnderson仅用于演示抽象。当然,递归蹦床是没有用的。
–贝尔吉
16-10-13在11:50
评论
是的,但是那仍然是递归的。 loopy不会溢出,因为它不会自行调用。“我原以为已经实施了总体拥有成本,但事实证明我错了。”大多数scenaros中至少在V8中已经存在。您可以通过告诉Node在V8中启用它,从而在任何最新版本的Node中使用它:stackoverflow.com/a/30369729/157247自Chrome 51起,Chrome就拥有了它(在“ experimental”标志之后)。
使用者的动能在蹦床下垂时转换为弹性势能,然后在反弹时又恢复为动能。
@immibis,代表所有来这里的人,没有检查这是哪个Stack Exchange站点,谢谢。
@jpaugh是指“跳跃”吗? ;-)