如何在JavaScript中理解蹦床?

时间:2014-08-10 13:03:00

标签: javascript recursion trampolines

以下是代码:

function repeat(operation, num) {
  return function() {
    if (num <= 0) return
    operation()
    return repeat(operation, --num)
  }
}

function trampoline(fn) {
  while(fn && typeof fn === 'function') {
    fn = fn()
  }
}

module.exports = function(operation, num) {
  trampoline(function() {
    return repeat(operation, num)
  })
}

我已经读过蹦床用于处理溢出问题,因此该函数不仅会保持调用自身和堆栈。

但这段代码如何运作?特别是trampoline功能? while它究竟做了什么,它是如何实现目标的呢?

感谢您的帮助:)

3 个答案:

答案 0 :(得分:119)

蹦床只是一种优化递归的技术,可以防止不支持tail call optimization的语言中的堆栈溢出异常,例如Javascript ES5实现和C#。但是,ES6可能会支持尾部调用优化。

常规递归的问题是每次递归调用都会向调用堆栈添加堆栈帧,您可以将其视为金字塔调用。以下是递归调用阶乘函数的可视化:

(factorial 3)
(* 3 (factorial 2))
(* 3 (* 2 (factorial 1)))
(* 3 (* 2 (* 1 (factorial 0)))) 
(* 3 (* 2 (* 1 1)))
(* 3 (* 2 1))
(* 3 2)
6

这是堆栈的可视化,其中每个垂直划线是堆栈帧:

         ---|---
      ---|     |---
   ---|            |--- 
---                    ---

问题是堆栈的大小有限,堆叠这些堆栈帧可能会溢出堆栈。根据堆栈大小,计算更大的阶乘会溢出堆栈。这就是为什么C#,Javascript等中的常规递归可以被认为是危险

最佳执行模型类似于 trampoline 而不是金字塔,其中每个递归调用都在适当的位置执行,并且不会堆叠在调用堆栈上。支持尾调用优化的语言中的执行可能如下所示:

(fact 3)
(fact-tail 3 1)
(fact-tail 2 3)
(fact-tail 1 6)
(fact-tail 0 6)
6

您可以将堆栈视为弹跳蹦床:

   ---|---   ---|---   ---|---
---      ---       ---       

这显然更好,因为堆栈始终只有一个帧,从可视化中你也可以看到为什么它被称为蹦床。这可以防止堆栈溢出。

由于我们在Javascript中没有tail call optimization的奢侈品,我们需要找到一种方法将常规递归转换为将以蹦床方式执行的优化版本。

一种显而易见的方法是去除递归,并重写代码以进行迭代。

如果不可能,我们需要更复杂的代码,而不是直接执行递归步骤,我们将利用higher order functions返回包装函数,而不是直接执行递归步骤,并让另一个函数控制执行。

在您的示例中, repeat 函数使用函数包装常规递归调用,并返回该函数而不是执行递归调用:

function repeat(operation, num) {
    return function() {
       if (num <= 0) return
       operation()
       return repeat(operation, --num)
    }
}

返回的函数是递归执行的下一步,而trampoline是一种在while循环中以受控和迭代的方式执行这些步骤的机制:

function trampoline(fn) {
    while(fn && typeof fn === 'function') {
        fn = fn()
    }
}

因此,trampoline函数的唯一目的是以迭代方式控制执行,并确保堆栈在任何给定时间在堆栈上只有一个堆栈帧。

使用蹦床显然不如简单递归那么高,因为你“阻塞”正常的递归流,但它更安全。

http://en.wikipedia.org/wiki/Tail_call

http://en.wikipedia.org/wiki/Trampoline_%28computing%29

答案 1 :(得分:6)

while循环将继续运行,直到条件为假。

如果fn && typeof fn === 'function'本身是假的,或者fn不是函数,那么

fn将是假的。

前半部分实际上是多余的,因为虚假值也不是函数。

答案 2 :(得分:1)

其他回复描述了蹦床的运作方式。给定的实现有两个缺点,其中一个甚至是有害的:

  • 蹦床协议仅取决于功能。如果递归操作的结果也是一个函数怎么办?
  • 您必须在整个调用代码中使用trampoline函数应用递归函数。这是一个应该隐藏的实现细节。

基本上,蹦床技术以热切评估的语言处理懒惰评估。这是一种避免上述缺点的方法:

&#13;
&#13;
// a tag to uniquely identify thunks (zero-argument functions)

const $thunk = Symbol.for("thunk");

//  eagerly evaluate a lazy function until the final result

const eager = f => (...args) => {
  let g = f(...args);
  while (g && g[$thunk]) g = g();
  return g;
};

// lift a normal binary function into the lazy context

const lazy2 = f => (x, y) => {
  const thunk = () => f(x, y);
  return (thunk[$thunk] = true, thunk);
};

// the stack-safe iterative function in recursive style

const repeat = n => f => x => {
  const aux = lazy2((n, x) => n === 0 ? x : aux(n - 1, f(x)));
  return eager(aux) (n, x);
};

const inc = x => x + 1;

// and run...

console.log(repeat(1e6) (inc) (0)); // 1000000
&#13;
&#13;
&#13;

延迟评估在repeat内进行。因此,您的主叫代码并不需要担心。