延续和callback有什么区别?
我一直在浏览整个networking,寻找关于延续的启示,想到最简单的解释会如此完全混淆像我这样的JavaScript程序员,令人惊讶。 当大多数文章用Scheme中的代码解释延续或使用monad时,情况尤其如此。
现在我终于认为我已经理解了延续的本质,我想知道我所知道的是否是事实。 如果我的想法是真实的,那么这是无知而不是启蒙。
所以,这就是我所知道的:
在几乎所有的语言中,函数显式地将值(和控制)返回给调用者。 例如:
var sum = add(2, 3); console.log(sum); function add(x, y) { return x + y; }
现在,在使用第一类函数的语言中,我们可以将控制和返回值传递给callback函数,而不是显式地返回给调用者:
add(2, 3, function (sum) { console.log(sum); }); function add(x, y, cont) { cont(x + y); }
因此,不是从函数返回值,而是继续使用另一个函数。 所以这个函数被称为第一个的延续。
那么延续和callback有什么区别呢?
我相信延续是callback的一个特例。 一个函数可以callback任意数量的函数,任意次数。 例如:
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); }
尽pipe写了很棒的文字,但我觉得你有点混淆你的术语。 例如,当调用是一个函数需要执行的最后一件事情时发生尾部调用是正确的,但是就延续而言,尾部调用意味着该函数不会修改所调用的延续,只是它更新传递给继续的值(如果需要的话)。 这就是为什么将尾recursion函数转换为CPS非常简单(只需将continuation作为参数添加并调用结果的延续)。
将继续称为callback的特殊情况也有点奇怪。 我可以看到他们是如何轻松地组合在一起,但延续并不是由于需要与callback区分开来的。 延续实际上代表了完成一个计算所剩下的指令 ,或者是从这个时间点开始的剩余计算。 你可以把延续视为一个需要填补的空缺。如果我能捕捉到一个程序目前的延续,那么当我捕捉延续时,我可以回到程序的确切程度。 (这确实使debugging器更容易编写。)
在这种情况下,你的问题的答案是callback是一个通用的东西,被调用者[callback]提供的某个契约指定的任何时间点被调用。 callback可以有任意多的参数,并以任何想要的方式进行结构化。 那么, 延续就必然是解决通过它的价值的一个论证程序。 必须将延续应用于单个值,并且应用程序必须在最后发生。 当继续执行完成时,expression式完成,根据语言的语义,可能产生或可能不产生副作用。
简单的回答是继续和callback之间的区别在于在调用callback(并且已经完成)之后执行在其被调用的点处恢复,而调用延续会导致执行在创build继续的点处恢复。 换句话说: 一个延续永远不会返回 。
考虑一下function:
function add(x, y, c) { alert("before"); c(x+y); alert("after"); }
(我使用Javascript语法,即使Javascript实际上并不支持一stream的延续,因为这是您的例子,对于不熟悉Lisp语法的人来说,这会更容易理解。)
现在,如果我们传递一个callback:
add(2, 3, function (sum) { alert(sum); });
那么我们会看到三个警报:“之前”,“5”和“之后”。
另一方面,如果我们要传递一个和callback一样的延续,就像这样:
alert(callcc(function(cc) { add(2, 3, cc); }));
那么我们只会看到两个提示:“之前”和“5”。 在add()
调用c()
结束add()
的执行并导致callcc()
返回; callcc()
返回的值是作为parameter passing给c
(即和)。
从这个意义上说,即使调用延续看起来像一个函数调用,但在某些方面更类似于返回语句或引发exception。
实际上,可以使用call / cc将返回语句添加到不支持它们的语言。 例如,如果JavaScript没有返回语句(而是像许多Lips语言一样,只是返回函数体中最后一个expression式的值),但是确实有call / cc,我们可以像这样实现返回值:
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()
返回在myArray
中findtarget
的索引i
。
(注意:“返回”类比有一些方法是简单的,例如,如果一个延续从它被创build的函数中逃逸出来 – 通过被保存在一个全局的地方,比如说 – 可能的话,函数即使它只被调用一次 ,创build延续的多次也可以返回 )。
Call / cc可以类似地用来实现exception处理(throw和try / catch),循环和许多其他的控制结构。
为了澄清一些可能的误解:
-
为了支持一stream的延续,尾巴呼叫优化不是必需的。 考虑一下,即使是C语言,也是以
setjmp()
的forms创build了一个continuation,而longjmp()
这样的longjmp()
连续的longjmp()
。- 另一方面,如果你天真地尝试编写你的程序继续传递风格没有尾巴调用优化,你注定最终溢出堆栈。
-
继续需要只有一个参数没有特别的理由。 只是继续的参数成为call / cc的返回值,而call / cc通常被定义为只有一个返回值,所以自然延续必须只有一个。 在支持多个返回值的语言(如Common Lisp,Go或者Scheme)中,完全有可能有接受多个值的延续。