【问题标题】:Is tail recursion merely a special case of CPS?尾递归仅仅是 CPS 的一个特例吗?
【发布时间】:2016-05-06 00:20:46
【问题描述】:

我以尾递归和延续传递方式实现了map。两个版本非常相似:

var inc = x => ++x;
var xs = [1,2,3,4,5];

var mapR = f => xs => {
    var rec = acc => {
        acc[acc.length] = f(xs[acc.length]);
        return acc.length < xs.length ? rec(acc) : acc;
    }

    return rec([]);
}

mapR(inc)(xs); // [2,3,4,5,6]

var mapC = f => xs => cc => {
    var rec = acc => cc => {
        acc[acc.length] = f(xs[acc.length]);
        return acc.length < xs.length ? cc(acc)(rec) : acc;
    }

    return cc(rec([])(rec));
}

mapC(inc)(xs)(console.log.bind(console)); // [2,3,4,5,6]

我显然也可以写rec(acc),而不是cc(acc)(rec)。我的结论是否正确,尾递归只是 CPS 的一个特例,而用 var rec = acc =&gt; {...} 编写的 mapC 是正确的 CPS 函数?

【问题讨论】:

  • 实际上,对于真正的 CPS,您永远不应该return。还假设f 继续。
  • @Bergi 但是() =&gt; { a(); } 变成了() =&gt; { a(); return undefined; } 因此a 将不再处于尾部位置,或者你的意思是什么?
  • 我的意思是 CPS 是 steriods 的尾调用,没有真正返回任何东西 - 你没有调用堆栈,你只是跳到下一个延续。当然,这需要一个特殊的解释器,这实际上是一种可怕的编程风格,但是已经构建了 poc :-)。仅当您从非 cps 代码调用 cps 时,returns 才有意义。

标签: javascript tail-recursion continuations


【解决方案1】:

为了能够回答问题,首先需要澄清术语:

  1. 递归:从同一个函数中调用一个函数
  2. 尾调用:函数返回前的最后一件事是调用另一个函数
  3. 尾递归:#1 和 #2 结合
  4. 直接风格:以函数为特征的顺序编程风格,返回给调用者
  5. Continuation Passing Style (CPS):编程风格的特点是带有附加 continuation 参数的函数,它调用它们的 continuation 而不是返回给调用者(continuation 只是 Javascript 中的函数)

这些术语的相关性如何?

  • 直接样式和继续传递样式是控制流的对立概念
  • 尾递归调用是尾调用的一种特殊化
  • 递归和尾递归是直接风格的技术
  • 每个(尾)递归算法都可以转换成它的 CPS 形式,因为 CPS 比递归更具有表达能力

比较尾递归和 CPS 没有意义,因为这两种技术代表了应该如何处理控制流的不同范例——即使它们非常相似:

  • 当然两者都可以描述递归控制流
  • 两者都没有调用堆栈
  • 但是:尾递归有一个静态的,CPS 一个动态的控制流(决定接下来调用哪个函数)

最后一点:描述递归算法的 CPS 函数将其数据存储在递归定义的匿名函数(闭包)环境中。这意味着,CPS 不会比递归更有效地使用内存。

【讨论】:

    【解决方案2】:

    我会在纯 CPS 中这样写:

    const inc = x => cont => cont(x+1);
    
    const map = f => xss => cont => {
        if (!xss.length) cont([]);
        else f(xss[0])(x => map(f)(xss.slice(1))(xs => cont([x].concat(xs))));
    };
    
    // or with an accumulator:
    const mapA = f => xs => cont => {
        const rec = acc => {
            if (acc.length >= xs.length) cont(acc);
            else f(xs[acc.length])(x => {
                acc.push(x);
                rec(acc);
            });
        }
        rec([]);
    };
    

    尾递归仅仅是CPS的一个特例吗?

    我不会这么说的。 CPS 与递归没有太大关系。
    然而,CPS 通常只包含尾调用,这使得堆栈变得多余 - 并且功能如此强大。

    【讨论】:

    • 是的,肯定只是错别字:-) 谢谢
    • map 使用调用堆栈为每次迭代临时存储xinc 的相应结果)。这不违反 TCO 原则吗?抱歉,但函数调用在尾部位置时会令人困惑,而不是......
    • 我会说闭包存储它 - x 的生命周期与延续的生命周期有关,而不是调用的生命周期。 f 在这里处于尾调用位置(基本上每个其他位置也是如此),堆栈帧可以立即回收。是的,这很可怕……
    猜你喜欢
    • 1970-01-01
    • 2011-02-18
    • 2016-04-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-28
    • 2020-11-12
    相关资源
    最近更新 更多