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

Javascript 尾部递归仅仅是CPS的一个特例吗?,javascript,tail-recursion,continuations,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; } r

我已经用尾部递归和连续传递样式实现了
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]
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]);
返回acc.lengthxs=>cc=>{
var rec=acc=>cc=>{
acc[acc.length]=f(xs[acc.length]);
返回acc.length

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

我将用纯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通常只包含尾部调用,这使得堆栈变得多余,而且功能也非常强大。

要回答这个问题,需要首先澄清以下术语:

  • 递归:从同一函数中调用函数
  • 尾部调用:函数返回前的最后一件事是调用另一个函数
  • 尾部递归:#1和#2组合
  • 直接风格:以函数为特征的顺序编程风格,这些函数返回给它们的调用者
  • Continuation Passing Style(CPS):编程风格,其特点是函数带有附加的Continuation参数,这些参数调用它们的Continuation,而不是返回调用方(Continuation只是Javascript中的函数)
  • 这些术语之间有什么关联

    • 直接式和连续传球式是控制流的对立概念
    • 尾部递归调用是尾部调用的特殊化
    • 递归和尾部递归是直接风格的技术
    • 每个(尾部)递归算法都可以转换为其CPS形式,因为CPS比递归具有更大的表达能力
    比较尾部递归和CPS没有意义,因为这两种技术代表了如何处理控制流的不同范例,即使它们有很多相似之处:

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

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

    实际上,对于真正的CPS,您不应该
    返回
    。还假设
    f
    需要一个延续。@Bergi但是
    ()=>{a();}
    变成
    ()=>{a();返回未定义;}
    因此
    a
    将不再处于尾部位置,或者你是什么意思?我的意思是CPS是steriods上的尾部调用,从来没有真正返回任何东西-你没有调用堆栈,你只要跳到下一个继续。当然,这需要一个特殊的解释器,这实际上是一种可怕的编程风格,但是已经构建了POC:-)。只有当您从非cps代码调用cps时,
    return
    s才有意义。是的,肯定只是输入错误:-)谢谢
    map
    使用调用堆栈临时存储每次迭代的
    x
    (相应的
    inc
    结果)。这是否与TCO原则相矛盾?很抱歉,当函数调用处于尾部位置时和不处于尾部位置时,这会让人感到困惑……我要说的是闭包存储了它,
    x
    的生存期绑定到延续的生存期,而不是调用的生存期
    f
    在这里处于尾部调用位置(基本上是每隔一个),堆栈帧可以立即回收。是的,很可怕…