我一直在浏览整个网络,寻找关于延续的启示,并且令人难以置信的是,最简单的解释如何能够如此完全混淆像我这样的JavaScript程序员。当大多数文章用Scheme中的代码解释延续或使用monad时,尤其如此。
现在我终于认为我已经理解了延续的本质,我想知道我所知道的是否真的是事实。如果我认为真实的事实并非如此,那么它就是无知而不是启蒙。
所以,这就是我所知道的:
在几乎所有语言中,函数显式地将值(和控制)返回给它们的调用者。例如:
var sum = add(2, 3);
console.log(sum);
function add(x, y) {
return x + y;
}
现在在具有第一类函数的语言中,我们可以传递控制并将值返回给回调而不是显式返回给调用者:
add(2, 3, function (sum) {
console.log(sum);
});
function add(x, y, cont) {
cont(x + y);
}
因此,我们不再从函数返回值,而是继续使用另一个函数。因此,此函数称为第一个函数的延续。
那么延续和回调之间的区别是什么?
答案 0 :(得分:159)
我认为延续是回调的特例。函数可以任意次数回调任意数量的函数。例如:
var array = [1, 2, 3];
forEach(array, function (element, array, index) {
array[index] = 2 * element;
});
console.log(array);
function forEach(array, callback) {
var length = array.length;
for (var i = 0; i < length; i++)
callback(array[i], array, i);
}
但是如果一个函数回调另一个函数作为它的最后一个函数,那么第二个函数被称为第一个函数的延续。例如:
var array = [1, 2, 3];
forEach(array, function (element, array, index) {
array[index] = 2 * element;
});
console.log(array);
function forEach(array, callback) {
var length = array.length;
// This is the last thing forEach does
// cont is a continuation of forEach
cont(0);
function cont(index) {
if (index < length) {
callback(array[index], array, index);
// This is the last thing cont does
// cont is a continuation of itself
cont(++index);
}
}
}
如果一个函数调用另一个函数作为它做的最后一个函数,那么它被称为尾调用。一些语言如Scheme执行尾调用优化。这意味着尾调用不会产生函数调用的全部开销。相反,它被实现为一个简单的goto(调用函数的堆栈帧被尾调用的堆栈帧替换)。
奖金:继续传递风格。请考虑以下程序:
console.log(pythagoras(3, 4));
function pythagoras(x, y) {
return x * x + y * y;
}
现在,如果每个操作(包括加法,乘法等)都以函数的形式编写,那么我们就会:
console.log(pythagoras(3, 4));
function pythagoras(x, y) {
return add(square(x), square(y));
}
function square(x) {
return multiply(x, x);
}
function multiply(x, y) {
return x * y;
}
function add(x, y) {
return x + y;
}
此外,如果我们不被允许返回任何值,那么我们将不得不使用如下的延续:
pythagoras(3, 4, console.log);
function pythagoras(x, y, cont) {
square(x, function (x_squared) {
square(y, function (y_squared) {
add(x_squared, y_squared, cont);
});
});
}
function square(x, cont) {
multiply(x, x, cont);
}
function multiply(x, y, cont) {
cont(x * y);
}
function add(x, y, cont) {
cont(x + y);
}
这种不允许返回值的编程风格(因此你必须求助于传递延续)称为延续传递风格。
继续传递风格有两个问题:
通过异步调用continuation,可以在JavaScript中轻松解决第一个问题。通过异步调用continuation,函数在调用continuation之前返回。因此调用堆栈大小不会增加:
Function.prototype.async = async;
pythagoras.async(3, 4, console.log);
function pythagoras(x, y, cont) {
square.async(x, function (x_squared) {
square.async(y, function (y_squared) {
add.async(x_squared, y_squared, cont);
});
});
}
function square(x, cont) {
multiply.async(x, x, cont);
}
function multiply(x, y, cont) {
cont.async(x * y);
}
function add(x, y, cont) {
cont.async(x + y);
}
function async() {
setTimeout.bind(null, this, 0).apply(null, arguments);
}
第二个问题通常使用名为call-with-current-continuation
的函数来解决,该函数通常缩写为callcc
。遗憾的是,callcc
无法在JavaScript中完全实现,但我们可以为其大多数用例编写替换函数:
pythagoras(3, 4, console.log);
function pythagoras(x, y, cont) {
var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
function square(x, cont) {
multiply(x, x, cont);
}
function multiply(x, y, cont) {
cont(x * y);
}
function add(x, y, cont) {
cont(x + y);
}
function callcc(f) {
var cc = function (x) {
cc = x;
};
f(cc);
return cc;
}
callcc
函数使用函数f
并将其应用于current-continuation
(缩写为cc
)。 current-continuation
是一个延续函数,它在调用callcc
之后将函数体的其余部分包装起来。
考虑函数pythagoras
的主体:
var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
第二个current-continuation
的{{1}}是:
callcc
同样,第一个function cc(y_squared) {
add(x_squared, y_squared, cont);
}
的{{1}}是:
current-continuation
由于第一个callcc
的{{1}}包含另一个function cc(x_squared) {
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
,因此必须将其转换为续传样式:
current-continuation
所以基本上callcc
逻辑上将整个函数体转换回我们开始的(并给那些匿名函数命名为callcc
)。使用callcc的这种实现的毕达哥拉斯函数变为:
function cc(x_squared) {
square(y, function cc(y_squared) {
add(x_squared, y_squared, cont);
});
}
同样,您无法在JavaScript中实现callcc
,但您可以在JavaScript中实现延续传递样式,如下所示:
cc
函数function pythagoras(x, y, cont) {
callcc(function(cc) {
square(x, function (x_squared) {
square(y, function (y_squared) {
add(x_squared, y_squared, cont);
});
});
});
}
可用于实现复杂的控制流结构,例如try-catch块,协同程序,生成器,fibers等。
答案 1 :(得分:26)
尽管写得很精彩,但我认为你的术语有点令人困惑。例如,当调用是函数需要执行的最后一件事时,发生尾调用是正确的,但是对于continuation,尾调用意味着函数不会修改它被调用的延续,只是它更新传递给continuation的值(如果需要)。这就是为什么将尾递归函数转换为CPS是如此简单(只需将continuation添加为参数并在结果上调用continuation)。
将continuation称为回调的特殊情况也有点奇怪。我可以看到它们如何轻松地组合在一起,但延续并不是因为需要区分回调。延续实际上表示剩余的 指令用于完成计算 ,或者来自此时间点的剩余计算。你可以把延续想象成一个需要填补的漏洞。如果我能捕捉到一个程序的当前延续,那么我可以回到我捕获延续时程序的确切方式。 (这确实使调试器更容易编写。)
在这种情况下,你的问题的答案是回调是一个通用的东西,可以在[回调]调用者提供的某个契约指定的任何时间点调用。回调可以包含任意数量的参数,并以任何方式构建。那么, continuation 必然是一个参数过程,它解析传递给它的值。必须将延续应用于单个值,并且应用程序必须在最后发生。当继续完成执行时,表达式已完成,并且,根据语言的语义,可能已生成或可能未生成副作用。
答案 2 :(得分:12)
简短的回答是延续和回调之间的区别在于,在调用回调(并且已经完成)之后,执行会在调用它时恢复,而调用继续会导致执行在继续执行时恢复创建了。换句话说:延续永不返回。
考虑一下这个功能:
function add(x, y, c) {
alert("before");
c(x+y);
alert("after");
}
(我使用Javascript语法,即使Javascript实际上不支持一流的延续,因为这是你给出的例子,对于不熟悉Lisp语法的人来说会更容易理解。)
现在,如果我们传递一个回调:
add(2, 3, function (sum) {
alert(sum);
});
然后我们会看到三个警告:“之前”,“5”和“之后”。
另一方面,如果我们传递一个与回调相同的延续,就像这样:
alert(callcc(function(cc) {
add(2, 3, cc);
}));
然后我们会看到两个警告:“之前”和“5”。在c()
内调用add()
会结束add()
的执行并导致callcc()
返回; callcc()
返回的值是作为c
的参数传递的值(即总和)。
从这个意义上讲,即使调用一个continuation看起来像一个函数调用,它在某些方面更类似于return语句或抛出异常。
实际上,call / cc可用于将return语句添加到不支持它们的语言中。例如,如果JavaScript没有return语句(相反,就像许多Lips语言一样,只返回函数体中最后一个表达式的值)但确实有call / cc,我们可以像这样实现return:
function find(myArray, target) {
callcc(function(return) {
var i;
for (i = 0; i < myArray.length; i += 1) {
if(myArray[i] === target) {
return(i);
}
}
return(undefined); // Not found.
});
}
调用return(i)
会调用一个延续,以终止匿名函数的执行,并导致callcc()
返回i
target
myArray
setjmp()
}。
(注意:在某种程度上,“返回”类比有点简单。例如,如果延续从它创建的函数中逃脱 - 通过保存在某个地方的某个地方,比如说 - 它是可能的创建延续的函数可以多次返回,即使它只被调用一次。)
Call / cc可以类似地用于实现异常处理(throw和try / catch),循环和许多其他控制结构。
澄清一些可能的误解:
为了支持一流的延续,无论如何都不需要尾调用优化。考虑到即使是C语言也有longjmp()
形式的(限制)形式的延续,它创建了一个延续,而{
"name": "Starship",
"plural": "starships",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {},
"acls": [],
"methods": {}
}
则调用了一个!{/ p>
没有特别的理由延续只需要一个参数。只是连续的参数成为call / cc的返回值,而call / cc通常被定义为具有单个返回值,所以自然地,continuation必须只取一个。在支持多个返回值的语言中(如Common Lisp,Go或者甚至是Scheme),完全有可能具有接受多个值的延续。