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

时间:2016-01-28 14:49:18

标签: javascript tail-recursion continuations

我已经在尾递归和连续传递样式中实现了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]

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

2 个答案:

答案 0 :(得分:1)

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

  1. 递归:从同一函数中调用函数
  2. 尾调用:函数返回前的最后一件事是调用另一个函数
  3. 尾递归:#1和#2合并
  4. 直接样式:以函数为特征的顺序编程风格,返回其调用者
  5. 继续传递风格(CPS):编程风格,其特征是具有附加延续参数的函数,调用它们的延续而不是返回其调用者(延续只是Javascript中的函数)
  6. 如何关联这些条款?

    • 直接风格和延续传递风格是控制流的对立概念
    • 尾递归调用是尾调用的专门化
    • 递归和尾递归是Direct Style
    • 的技术
    • 每个(尾部)递归算法都可以转换为CPS形式,因为CPS比递归具有更强的表达能力

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

    • 两者当然可以描述递归控制流程
    • 两者都没有调用堆栈
    • 但是:Tail递归有一个静态,CPS是一个动态控制流程(解析下一个函数被调用)

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

答案 1 :(得分:0)

我在纯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通常只包含尾调用,这使得堆栈变得多余 - 而且函数功能如此强大。