我已多次尝试掌握continuations和call/cc的概念。每一次尝试都是失败的。有人可以向我解释这些概念,理想情况下,这些概念比维基百科或其他SO帖子更具现实性。
我有网络编程和OOP的背景。我也理解6502汇编并且与Erlang有一个小的randez-vous。不过,我无法绕过电话/ cc。
答案 0 :(得分:22)
要将它与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堆栈和延续之间的一个关键区别是,即使堆栈的状态发生了变化,也可以在程序的任何一点使用延续。这意味着您可以基本上恢复堆栈的早期版本并一次又一次地使用它们,从而产生一些独特的程序流。
(* 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实现自己的线程调度程序,因为我试图说明here。
答案 1 :(得分:12)
看,我已经找到了关于这个主题的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会大大简化。您只需将延期注册为回调,并在请求完成时恢复计算。
答案 2 :(得分:8)
使用continuation的一个简单例子将在单处理器计算机上实现一个线程(光纤,如果你愿意)管理器。调度程序将定期中断执行流程(或者,在光纤的情况下,在代码中的各个战略点调用),保存延续状态(对应于当前线程),然后切换到另一个继续状态(对应于之前保存状态的其他线程。)
参考你的装配背景,继续状态将捕获诸如指令指针,寄存器和堆栈上下文(指针)之类的详细信息,以便随意保存和恢复。
使用continuation的另一种方法是想到用几个类似线程的实体替换方法调用并行共存(运行或挂起)使用continuation上下文相互传递控制“经典”call
范式。它们将对全局(共享)数据进行操作,而不是依赖于参数。这在某种程度上比call
更灵活,因为堆栈不必最终向下(calls
嵌套),但控制可以任意传递。
尝试用C这样的语言可视化这个概念,假设有一个带有单个switch(continuation_point) { case point1: ... }
语句的大循环,其中每个case
对应一个continuation-savepoint每个case
中的代码可以改变continuation_point
的值,并通过continuation_point
break
放弃对switch
的控制权迭代循环。
您问题的背景是什么?您感兴趣的任何特定场景?任何特定的编程语言?线程/光纤示例是否足够?
答案 3 :(得分:5)
对我有帮助的是这样一种想法,即在使用函数调用的传统语言中,只要进行函数调用,就会隐式传递一个延续。
在跳转到函数的代码之前,你会在堆栈中保存一些状态(即你推送你的返回地址,而堆栈已经包含你的本地人)。这基本上是一个延续。当函数完成时,它必须确定将执行流发送到何处。它使用存储在堆栈中的延续,弹出返回地址并跳转到它。
其他语言概括了这种继续的概念,允许您明确指定继续执行代码的位置,而不是从函数调用的位置隐式继续。
根据评论编辑:
延续是完整的执行状态。在任何执行点,你可以将程序分成两部分(在时间上,而不是空间) - 已经运行到这一点的部分,以及将从这里运行的所有部分。 “当前的延续”是“从这里开始运行的所有东西”(你可以认为它有点像一个函数,可以完成你的其他程序所做的一切)。因此,您向call/cc
提供的函数将被传递给调用call/cc
时的当前延续。该函数可以使用continuation将执行返回到call/cc
语句(更可能的是它会将继续传递给其他东西,因为如果它直接使用它,它可以做一个简单的返回)。
答案 4 :(得分:4)
当我试图理解来电/ cc时,我发现这个call-with-current-continuation-for-C-programmers页面很有帮助。
答案 5 :(得分:3)
我见过的最好的解释是保罗格雷厄姆的书,On Lisp。
答案 6 :(得分:3)
想象一下,你的剧本是一个视频游戏阶段。 Call / cc就像一个奖励阶段。
一旦你触摸它,你就会被转移到奖励阶段(即函数的定义作为参数传递给call / cc [在这种情况下为f])。
奖金阶段与普通阶段不同 因为通常他们有一个元素(即传递给call / cc的函数的参数),如果你触摸它就输了并被运回正常阶段。
所以如果有很多args
,当你到达其中一个时,它无关紧要。因此,我们的执行到达(arg 42)
并将其返回到总和(+ 42 10)
。
还有一些值得注意的评论:
(define f (lambda (k) (+ k 42))
,因为你不能sum
a
功能。 (define f (lambda (k) (f 42 10)))
因为continuation只需要一个参数。 touching
任何退出,在这种情况下函数继续像
任何普通的函数(例如(define f (lambda (k) 42)
完成和
返回42)。答案 7 :(得分:2)
理解call / cc有多个级别。 首先,您需要了解术语以及机制的工作原理。 然后了解在现实生活中如何以及何时使用call / cc 编程是必要的。
通过研究CPS可以达到第一级,但也有 的替代品。
对于第二级,我推荐弗里德曼的以下经典作品。
Daniel P. Friedman。 “Continuations的应用:邀请教程”。 1988年编程语言原则(POPL88)。 1988年1月。
答案 8 :(得分:2)
查看FScheme的call / cc的描述和实现:http://blogs.msdn.com/b/ashleyf/archive/2010/02/11/turning-your-brain-inside-out-with-continuations.aspx
答案 9 :(得分:2)
我用于从命令性角度理解延续的模型是它是一个调用栈的副本,它与指向下一条指令的指针相结合。
Call / cc调用一个函数(作为参数传递),并将continuation作为参数。
答案 10 :(得分:0)
您可能已经熟悉“控制权转移”的概念,在C之类的语言中,它在break
,continue
,return
和{ {1}},或者-在支持例外的语言中-goto
和try
语句。
您可以想象catch
和break
可以使用continue
来实现(即,对于使用goto
或break
的每一段代码,您可以轻松编写使用continue
和适当放置的标签的等效代码。
因此,现在让我们集中讨论goto
,正如您从汇编的经验中应该知道的那样,goto
是最基本的控制传递操作(您可以想象很难转换return
使用goto
-但我们将继续进行)。
因此,假设您有一个看起来像这样的程序(例如,用C语言编写):
instruction1;
instruction2;
...
instructionN;
其中instructionK
可以是赋值或函数调用,也可以是语句if (condition) goto some_label
。
您可以在每行前添加goto
的唯一标签:
line1: instruction1;
line2: instruction2;
...
lineN: instructionN;
在支持一流延续性的语言中,有一个特殊的功能call/cc
,其工作原理如下:假设instructionK
的形式为
...
lineK: call/cc(function(continuation) { ... })
lineK+1: instructionK+1;
...
我在这里对匿名函数使用了JavaScript的符号,因为C不支持匿名函数。您可以看到该函数具有一个参数,我将其称为continuation
。
调用call/cc
时将立即执行函数的主体,并且continuation
参数的值将是lineK+1
的地址(大致而言)。或者,换句话说,lineK
中的当前续行是lineK+1
-您可以这样考虑。
但是,典型的接口是它不仅仅是地址:continuation
参数是一个过程,当调用该过程时,它会跳转到lineK+1
。这就是call/cc
允许实现return
语句的方式。
因此您可以将call/cc
看作是类固醇上的goto
。事实是,您不仅可以调用continuation
参数,还可以将其存储在变量或其他数据结构中。
我看到的call/cc
最有趣的用法是Dorai Sitaram的书Teach Yourself Scheme in Fixnum Days中的Amb评估程序的实现(您可以将其与Structure and Interpretation of Computer Programs中的版本比较),不要使用call/cc
)。
我曾经使用here描述了一种使用延续的资源管理机制。
但是除此之外,一流的延续还受到批评,我不建议在生产代码中使用它们(它们与C中可用的setjmp/longjmp机制非常相似,我也不鼓励这样做)但是,如果您想看一些用法示例,可以使用here来用它在100行od代码中实现多任务处理。