我已多次尝试掌握continuation和call/cc的概念.每一次尝试都是失败的.有人可以向我解释这些概念,理想情况下,这些概念比维基百科或其他SO帖子更具现实性.
我有网络编程和OOP的背景.我也理解6502汇编并且与Erlang有一个小的randez-vous.不过,我无法绕过电话/ cc.
要将它与C进行比较,当前的延续类似于堆栈的当前状态.它具有等待当前函数结果的所有函数,以便它们可以继续执行.捕获为当前延续的变量像函数一样使用,除了它获取提供的值并将其返回到等待堆栈.此行为类似于C函数longjmp,您可以立即返回到堆栈的下半部分.
(define x 0) ; dummy value - will be used to store continuation later (+ 2 (call/cc (lambda (cc) (set! x cc) ; set x to the continuation cc; namely, (+ 2 _) 3))) ; returns 5 (x 4) ; returns 6
C堆栈和延续之间的一个关键区别是,即使堆栈的状态已更改,也可以在程序中的任何位置使用continuation.这意味着您可以基本上恢复堆栈的早期版本并反复使用它们,从而产生一些独特的程序流.
(* 123 (+ 345 (* 789 (x 5)))) ; returns 7 reason: it is because (x 5) replaces the existing continuation, (* 123 (+ 345 (* 789 _))), with x, (+ 2 _), and returns 5 to x, creating (+ 2 5), or 7.
保存和恢复程序状态的能力与多线程有很多共同之处.事实上,你可以使用continuation实现自己的线程调度器,因为我已经试图说明这里.
看,我发现这个Continuation Passing Style这个主题的最佳描述.
这里删除了该文章的详细副本:
作者:Marijn Haverbeke日期:2007年7月24日
Scheme的call-with-current-continuation函数可以捕获计算,调用堆栈的状态,并在以后恢复相同的状态.除了这样的原语之外,还可以实现各种形式的异常处理和类似C的longjmp技巧.
function traverseDocument(node, func) { func(node); var children = node.childNodes; for (var i = 0; i < children.length; i++) traverseDocument(children[i], func); } function capitaliseText(node) { if (node.nodeType == 3) // A text node node.nodeValue = node.nodeValue.toUpperCase(); } traverseDocument(document.body, capitaliseText);
这可以转换如下:我们为每个函数添加一个额外的参数,用于传递函数的延续.此延续是一个函数值,表示函数"返回"后必须执行的操作.(调用)堆栈在延续传递样式中变得过时 - 当一个函数调用另一个函数时,这是它做的最后一件事.它不是等待被调用函数返回,而是将它想要做的任何工作放到一个延续中,然后传递给函数.
function traverseDocument(node, func, c) { var children = node.childNodes; function handleChildren(i, c) { if (i < children.length) traverseDocument(children[i], func, function(){handleChildren(i + 1, c);}); else c(); } return func(node, function(){handleChildren(0, c);}); } function capitaliseText(node, c) { if (node.nodeType == 3) node.nodeValue = node.nodeValue.toUpperCase(); c(); } traverseDocument(document.body, capitaliseText, function(){});
想象一下,我们有一个huuuuge文档可以资本化.只需一次性遍历它需要五秒钟,并且将浏览器冻结五秒钟是相当糟糕的风格.考虑对capitaliseText的这个简单修改(不要注意丑陋的全局):
var nodeCounter = 0; function capitaliseText(node, c) { if (node.nodeType == 3) node.nodeValue = node.nodeValue.toUpperCase(); nodeCounter++; if (nodeCounter % 20 == 0) setTimeout(c, 100); else c(); }
现在,每20个节点,计算中断一百毫秒,以便为浏览器界面提供响应用户输入的时刻.一种非常原始的线程形式 - 你甚至可以像这样同时运行多个计算.
一个更常用的应用程序与XMLHttpRequests或用于模拟它们的各种IFRAME和SCRIPT标记hacks有关.这些总是需要一个人使用某种回调机制来处理服务器发回的数据.在简单的情况下,一个简单的函数可以做,或者可以使用一些全局变量来存储在数据返回后必须恢复的计算状态.对于复杂的情况,例如当函数使用必须向其调用者返回某个值的函数时,continuation会大大简化.您只需将延续注册为回调,并在请求完成时恢复计算.
使用continuation的一个简单例子是在单处理器机器上实现一个线程(光纤,如果你愿意)管理器.调度程序将定期中断执行流程(或者,在光纤的情况下,在代码中的各个战略点调用),保存继续状态(对应于当前线程),然后切换到不同的延续状态(对应于一个不同的线程,其状态先前已保存.)
参考您的程序集背景,继续状态将捕获诸如指令指针,寄存器和堆栈上下文(指针)之类的详细信息,以便随意保存和恢复.
使用continuation的另一种方法是考虑用几个类似线程的实体替换方法调用,这些实体并行(运行或暂停)共存,使用延续上下文而不是"经典" call
范例将控制传递给对方.它们将对全局(共享)数据进行操作,而不是依赖于参数.这是在一定程度上比更灵活call
在堆栈不必卷起然后向下感测(calls
是嵌套的),但可以控制任意地绕过.
试图显现此概念的语言这样的C,想象具有带有单一一个大循环switch(continuation_point) { case point1: ... }
语句,其中每个case
对应于继续保存点,并且其中内每个码case
可以改变的值continuation_point
和放弃控制到continuation_point
由break
从荷兰国际集团在switch
与从事循环的下一次迭代.
你问题的背景是什么?您感兴趣的任何特定场景?任何特定的编程语言?螺纹/纤维示例是否足够?
假设您的脚本是一个视频游戏阶段。通话/抄送就像一个奖励阶段。
触摸后,您将立即进入奖励阶段(即,作为参数传递给call / cc [在这种情况下为f]的函数的定义)。
奖金阶段与普通阶段不同,因为奖金阶段通常具有一个元素(即,传递给call / cc的函数的参数),如果您触摸该元素,则会丢失并转移回正常阶段。
因此,是否有很多都没有关系args
,当您到达其中之一时就结束了。因此,我们的执行达到(arg 42)
并返回到总和(+ 42 10)
。
还有一些值得注意的注意事项:
并非所有功能都可以与call / cc一起使用。由于它需要一个延续(即一个函数),因此不能有这样的f:,
(define f (lambda (k) (+ k 42))
因为您不能sum
函数。
您也不能拥有,(define f (lambda (k) (f 42 10)))
因为延续只需要一个参数。
您可能没有touching
任何退出就完成操作,在这种情况下,该函数的执行与任何普通函数一样(例如,(define f (lambda (k) 42)
完成并返回42)。
对我有帮助的是这样一种想法,即在使用函数调用的传统语言中,只要进行函数调用,就会隐式传递一个延续.
在跳转到函数代码之前,您可以在堆栈中保存一些状态(即,您推送返回地址,堆栈已包含您的本地人).这基本上是一个延续.当函数完成时,它必须确定将执行流发送到何处.它使用存储在堆栈中的延续,弹出返回地址并跳转到它.
其他语言概括了这种继续的概念,允许您明确指定继续执行代码的位置,而不是隐式地继续进行函数调用.
根据评论编辑:
延续是完整的执行状态.在任何执行点,你可以将程序分成两部分(在时间上,而不是空间) - 已经运行到这一点的部分,以及将从这里运行的所有部分."当前的延续"是"从这里开始运行的所有东西"(你可以认为它有点像一个函数,可以完成你的其他程序所做的一切).因此,您提供的函数call/cc
将传递call/cc
调用时的当前延续.该函数可以使用continuation将执行返回到call/cc
语句(更可能的是它将继续传递给其他东西,因为如果它直接使用它,它可以做一个简单的返回).