我正在研究语言球拍并试图掌握/ cc实际上是为了什么。有人可以用简单的语言解释一下,举一两个例子吗?感谢。
答案 0 :(得分:9)
如果您有一个表达式(+ (* 2 3) (/ 10 2))
,则Scheme系统不会同时评估所有内容,而是部分评估。方案没有在Scheme中指定,但让我们想象它是从左到右(我认为Racket总是从左到右):
您执行(* 2 3)
,继续计算(/ 10 2)
,然后(+ result1 result2)
。 Scheme系统可以这样做的方法是在执行之前将代码转换为Continuation passing style。上面的表达式变成了这样的东西:
(lambda (k)
(*& 2
3
(lambda (result1)
(/& 10
2
(lambda (result2)
(+& result1 result2 k))))))
现在,最后&
的过程与Scheme中的过程相同,除了它是最后一个参数的延续。其中一个示例:(define (+& a b k) (k (+ a b)))
。所有其他的都是这样完成的,被认为是原始的。
如果您申请并传递display
或values
,则会显示或评估为11。
但是,如果您使用call/cc
,则可以覆盖它。想象一下这个变种:
(call/cc (lambda (k) (+ (* 2 3) (/ 10 (if (zero? a) (k +inf.0) a))
在Scheme中,如果除以零,则会出现错误,如果发生这种情况,您需要取消其余的计算,并说结果是无限的。上面的代码就是这样,如果a
为零,它将不会对前两个计算的结果求和。它实际上充当GOTO。该代码的CPS版本如下所示:
(lambda (k)
(*& 2
3
(lambda (result1)
(zero?& a
(lambda (azero?)
(if azero?
(k +inf.0) ; continuation used here
(/& 10
a
(lambda (result2)
(+& result1 result2 k))))))))) ; and here
那么call/cc
做了什么?那么它可以让你按常规方式编码,而不是CPS就像计算机如何进行实际计算,但是通过掌握延续来获得最佳的两个世界,这样你就可以像在CPS中编写它一样。
现在,想象一下这段代码:
(let* ((c 10)
(r (call/cc (lambda (exit) exit))))
(display "Hello\n")
(cond ((zero? c) 'done)
(else (set! c (- c 1))
(r r))))
在这里,您看到我将续集作为r
返回。继续是其余的计算,从设置r
开始,然后执行display
...这实际上显示“hello \ n”11次,因为当你在底部调用延续时它会做同样的事情一遍又一遍。
就像eval
一样,我确实试着将它保持在绝对最小值,当我这样做时,我通常会这样做以从正在进行的计算中获得中止。例如。
(define (lstadd1 lst)
(call/cc (lambda (exit)
(let loop ((lst lst))
(cond ((pair? lst) (cons (add1 (car lst)) (loop (cdr lst))))
((null? lst) '())
(else (exit #f))))))) ; not proper
(lstadd1 '(1 2 3)) ; ==> (2 3 4)
(lstadd1 '(1 2 . 3)) ; ==> #f
有关更多示例I love Matt Mights page,其中有很多关于如何使用continuation的示例。
答案 1 :(得分:6)
并非call/cc
的所有实现都完全相同,但希望这个答案适用于包括Racket在内的所有常见变体。这个故事实际上是基于Unlambda内置的c
。
你是一位超级英雄考古学家,在一个古老的玛雅遗址上挖掘。您将在优雅的拱门中挖掘出精美的,保存完好的石头门廊。但它只是一个门口,已经摆在一边;它附近没有任何墙壁,它似乎也不是墙壁的一部分。您的员工会从中获取能量读数,因此您将其完整地运送回实验室进行学习。
在你的实验室里,墙上挂着一个大钟,直接放在现在直立的拱门前面,你已放置在房间中间附近。在检查拱门时,你会走过它。
下午4:17
走过拱门之后,你会惊讶地发现,你手里拿着一个立方体盒子,大到足以放入一本书,里面有一个盖子和一个发光的按钮。你不记得拿起这样的盒子,或者之前看过它。你打电话给你的助手,询问盒子的来源。助手不知道。如果放下盒子,按钮会停止发光。你再次拿起它,按钮再次发光。只有握住它才会发光;如果助手拿起盒子就不会发光。在百灵鸟上,你将一个回形针放入盒子里,盖上盖子,然后按下按钮。
下午4:23
走过拱门之后,你会惊讶地发现,你手里拿着一个回形针。你不记得把它捡起来。你的助手在房间里,盯着你傻眼了。你不记得你的助手进入房间。你的时钟似乎也快几分钟。
"刚刚发生了什么?"问助手。
"我刚走过拱门,"你说,不是真的理解这个问题。
"不,你没有!盒子里发生了什么?"助手说。
"你在说什么?"你说,越来越恼火。
...
一旦整个情况发生,你就会了解这个拱门的作用,你决定做一些大胆的事情。你大声朗读当前时间,然后走过拱门。
下午5:30
从拱门出现,你拿着一个空盒子。观察时间是您预期的下午5:30,您可以在标有#1
的方框中附上便条。你把盒子放在桌子上,大声朗读当前时间,再次走过拱门。
下午5:31
从拱门出现,你拿着一个空盒子。观察到时间是您预期的下午5:31,您可以在标有#2 - use to forget
的框中附加便利贴。您将#2 框放在框#1中(它缩小到它的大小的一小部分;看起来这些框是为此设计的。)
作为一名超级英雄考古学家,你可以适当地装备自己并闯入你最不喜欢的压迫政权的外国大使馆,破坏它的金库并窃取一些密切保存的国家机密的纸质副本。香港动作片的东西,子弹和拳头飞舞。你将自己设置在金库中,在他们的警卫到达你之前购买宝贵的秒钟。从你的包里拿出#1盒子,你把纸张塞进里面(同时也不在里面的#2盒子里面),关闭盒子#1,就像金库门被打开一样,你微笑,拉别针,放下手榴弹,按下按钮。
晚上9:45
从拱门出现,你拿着一个空盒子,上面标有#2 - use to forget
和含有你最不喜欢的压迫政权敏感国家机密的文件。您还注意到时间不是您预期的下午5:30,而是下午9:45,因此您不会继续执行预定的闯入计划。您坐下来将文档的内容提交到内存中,一旦您确定将它们完全记忆,就可以将文档刻录到垃圾箱中。现在你大声朗读当前的时间并走过拱门。
凌晨2:00
从拱门出现,你拿着一个空盒子。观察当前时间是您预期的凌晨2:00,您可以立即标记新框#3 - use to remember
。你给自己写了一个简短的注释:Allow self to forget again. At exactly 7:15 PM call police and report suspicious persons at 14th and Maple.
放置方框#3和方框#2中的注释,然后点击方框#2上的按钮。
凌晨2:01
从拱门出现,你拿着一个空盒子,上面标有#3 - use to remember
,还有一张你自己手写的便条。时间比预期的下午5:31晚得多,因此您不会继续执行预定的闯入计划。按照您的指示,再次走过拱门,获取一个标有#4 - use to forget
的新框。你按照你的笔记中的指示打电话给警察,不知道为什么,并且在新闻日之后听到你家乡的国际间谍圈被打破了。
任务完成! (你假设。)从现在开始,你可以选择了解信息本身,但不知道你用它做了什么;或者不知道这些信息,但要知道它是如何被使用的。
最后,这个奇妙的工具付出了代价。当你继续在各种活动中依赖拱门时,你必须知道你永远会破坏你的生活,除非你在许多替代自我之间发送信息,否则碎片永远不会统一。单按一下按钮盒就会导致许多珍贵记忆的无法恢复,这可能是一种策略,也可能是一种严重而悲惨的错误。
穿过拱门代表call/cc
操作。这样做会导致创建一个新的按钮盒,它代表一个延续功能。将内容放入框中表示将参数传递给continuation函数。按下框上的按钮表示调用延续功能。
按下框上的按钮(调用延续函数)会导致传说中的字符(带变量的调用堆栈)恢复到创建框(继续)时的确切状态,即{{1最初完成了。传递给continuation的参数成为堆栈恢复后原始call/cc
的结果值;这就是为什么按下按钮时,传说中的角色不是一个盒子而是盒子的内容。
Boxes(continuations)可以封装(相互传递),允许使用call/cc
作为基元来实现协同例程,状态机和其他高级构造。
也可以使用Continuations轻松地从代码分支中以不合适的方式逃脱(删除手榴弹,按下按钮等),例如立即退出深层嵌套的条件和循环副作用。
另请注意,拱门不是时间机器;时间永远不会在故事中逆转自己,传说中的人物也不会穿过拱门。 (通过调用延续函数,不会颠倒破坏性赋值,堆内存的更改和其他副作用。)
练习:为要跟随的传说中的角色编写伪代码,这将导致正确执行的间谍计划。