我们可以实现尾递归模的缺点吗?通过蹦床?

时间:2019-05-02 17:14:02

标签: javascript recursion functional-programming compiler-optimization tail-recursion

您可以将蹦床视为程序中已实现的编译器优化。因此,是什么使我们无法以完全相同的方式采用更通用的优化技术。

这是尾递归模缺点的草图:

const loop = f => {
  let step = f();

 while (step && step[step.length - 1] && step[step.length - 1].type === recur) {
    let step_ = step.pop();
    step.push(...f(...step_.args));
  }

  return step;
};

const recur = (...args) =>
  ({type: recur, args});

const push = (xs, x) => (xs.push(x), xs);

const map = f => xs =>
  loop((i = 0) =>
    i === xs.length
      ? []
      : push([f(xs[i])], recur(i + 1)));

const xs = 
  map(x => x * 2) (Array(1e6).fill(0).map((x, i) => i))
  .slice(0,5);

console.log(xs); // [0, 2, 4, 6, 8]

这种优化取决于表达式的关联性。乘法也具有关联性,因此存在尾递归模乘。但是,我必须作弊才能在Javascript中实现它:

const loop = f => {
  let step = f();
  const acc = [];

  while (step && step[1] && step[1].type === recur) {
    acc.push(step[0]);
    step = f(...step[1].args);
  }

  return acc.reduce((acc, f) => f(acc), step);
};

const recur = (...args) =>
  ({type: recur, args});

const mul = x => step => [y => x * y, step];

const pow = (x_, n_) =>
  loop((x = x_, n = n_) =>
    n === 0 ? 1
    : n === 1 ? x
    : mul(x) (recur(x, n - 1)));
    
console.log(
  pow(2, 1e6)); // Infinity, no stack overflow

如您所见,我不能使用常规的mul,这并不是特别令人满意。这与Javascript蜜蜂语言紧密相关吗?有没有一种更好的方法可以在JS中实现尾递归模乘而不用引入笨拙的二进制运算符?

1 个答案:

答案 0 :(得分:1)

与其使用loop / recur(我认为这是一种丑陋且不必要的技巧),不如考虑使用folds:

const recNat = (zero, succ) => n => {
    let result = zero;

    while (n > 0) {
        result = succ(result);
        n = n - 1;
    }

    return result;
};

const mul = x => y => x * y;

const pow = x => recNat(1, mul(x));

console.log([0,1,2,3,4,5,6,1e6].map(pow(2))); // [1,2,4,8,16,32,64,Infinity]

几乎所有递归函数都可以使用折叠来定义(也称为结构递归,也称为归纳)。例如,甚至Ackermann function都可以使用折叠来定义:

const recNat = (zero, succ) => n => {
    let result = zero;

    while (n > 0) {
        result = succ(result);
        n = n - 1;
    }

    return result;
};

const add = x => y => x + y;

const ack = recNat(add(1),
    ackPredM => recNat(ackPredM(1),
        ackMPredN => ackPredM(ackMPredN)));

console.time("ack(4)(1)");
console.log(ack(4)(1)); // 65533
console.timeEnd("ack(4)(1)");

上面的代码段大约需要18秒才能在笔记本电脑上计算出答案。

现在,您可能会问为什么我使用迭代而不是自然递归来实现recNat

const recNat = (zero, succ) => function recNatZS(n) {
    return n <= 0 ? zero : succ(recNatZS(n - 1));
};

我之所以使用迭代,是因为您使用了迭代来实现loop。踩踏。通过为要折叠的每种数据类型实现不同的蹦床,您可以编写功能代码,而不必担心堆栈溢出。

底线::使用折叠而不是显式递归。它们比您想像的强大得多。