蹦床为什么起作用?


104

我一直在做一些功能性JavaScript。我以为已经实现了尾叫优化,但是事实证明我错了。因此,我不得不自学流浪汉。在这里和其他地方稍作阅读后,我便掌握了基础知识并构造了我的第一个蹦床:

/*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()导致堆栈溢出?而且不是从foo = foo()技术上讲变异,还是我错过了一些东西?也许这只是必要的邪恶。或者我缺少一些语法。

有没有办法了解它?还是只是某种技巧可以奏效?我已经能够通过其他所有方法,但是这让我感到困惑。


5
是的,但是那仍然是递归的。loopy不会溢出,因为它不会自己调用
tkausl

4
“我原以为已经实施了总体拥有成本,但事实证明我错了。” 大多数scenaros中至少在V8中已经存在。你可以告诉节点在任何新版本的节点使用它的实例来启用它在V8:stackoverflow.com/a/30369729/157247 Chrome的有它(后面的“实验性”的标志),因为Chrome的51
TJ克劳德

125
当蹦床下垂时,来自用户的动能转化为弹性势能,然后在反弹时又转化为动能。
immibis

66
@immibis,代表所有来这里的人,没有检查这是哪个Stack Exchange站点,谢谢。
user1717828

4
@jpaugh是指“跳跃”吗?;-)
绿巨人

Answers:


89

您的大脑反抗该功能的原因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)啊 好吧,它小于1000万,所以我们得到了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循环。


1
评论不作进一步讨论;此对话已转移至聊天
扬尼斯

它实际上只是一个求和类型。有时称为变体。动态语言相当容易地支持它们,因为每个值都被标记了,而更多的静态类型的语言将需要您指定函数返回一个变体。例如,在C ++或Haskell中,蹦床很容易实现。
GManNickG

2
@GManNickG:是的,这就是我的意思,“输入更多内容”。在C语言中,您必须声明一个联合,声明一个对联合进行标记的结构,在任一端对结构进行打包和解压缩,在任一端对结构进行打包和解压缩,并(大概)弄清楚谁拥有该结构所驻留的内存。C ++的代码可能要比C少,但是从概念上讲,它并不比C复杂,并且比OP的Java脚本还要冗长。
凯文

当然,我没有提出异议,我只是认为您对它的怪异或不合理的强调有点强。:)
GManNickG '16

173

凯文简洁地指出了这个特定的代码片段是如何工作的(以及为什么它很难理解),但是我想补充一些有关蹦床在一般情况下如何工作的信息。

如果没有尾部调用优化(TCO),则每个函数调用都会向当前执行堆栈中添加一个堆栈帧。假设我们有一个函数可以打印出数字倒数:

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

Trampolining通过在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对于绕开硬递归限制非常有用,例如,当语言实现设置最大调用堆栈大小时。


18

也许使用专用的返回类型(而不是滥用功能)来实现蹦床:

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));

将此与您的的版本进行比较trampoline,其中递归情况是函数返回另一个函数时的情况,而基本情况是它返回其他内容时的情况。

是什么foo = foo()导致堆栈溢出?

它不再自称。相反,它返回一个结果(在我的实现中,字面是a Result),该结果传达是继续递归还是中断。

而且不是从foo = foo()技术上讲变异,还是我错过了一些东西?也许这只是必要的邪恶。

是的,这恰恰是循环的必要弊端。一个人也可以写trampoline而不会产生突变,但是它需要再次递归:

function trampoline(fn, x) {
    const res = fn(x);
    if (res instanceof Recurse)
        return trampoline(fn, res.arg);
    else if (res instanceof Return)
        return res.value;
}

尽管如此,它仍显示出蹦床功能甚至更好的想法。

践踏的重点是将想要使用递归的函数的尾递归调用抽象为返回值,然后仅在一个位置进行实际的递归-该trampoline函数可以在单个位置进行优化以使用环。


foo = foo()从修改局部状态的意义上讲,这是变异,但我通常会认为重新分配是因为您实际上并未在修改底层函数对象,而是将其替换为其返回的函数(或值)。
JAB

@JAB是的,我并不是要隐式地更改foo包含的值,只修改变量。一个while循环需要一些可变的状态,如果你希望它终止,在此情况下,变量foox
Bergi '16

我做了这样的事情前阵子在这个答案至尾调用优化,蹦床等一个堆栈溢出问题
约书亚·泰勒

2
您的无突变版本已将的递归调用转换fn为的递归调用trampoline-我不确定这是否有所改善。
Michael Anderson

1
@MichaelAnderson仅用于演示抽象。当然,递归蹦床是没有用的。
Bergi
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.