我正在尝试查找call / cc的实现方式。我发现的最好的是这个Haskell片段:
callCC f = Cont $ \k -> runCont (f (\a -> Cont $ \_ -> k a)) k
虽然由于Cont
和runCont
而不是我想要的那么简单。我也找到了它的功能描述,尽管从未像实际代码那样清晰。
那么它是如何以最简单的形式实现的?我用Scheme和Haskell标记这个,因为这是我喜欢的两种语言。
答案 0 :(得分:80)
"实施call/cc
"在你工作的那个层面上真的没有意义;如果你能用一种语言实现call/cc
,那就意味着它有一个内置的构造,至少和call/cc
一样强大。在语言本身的层面上,call/cc
基本上是一个原始的控制流操作符,就像某种形式的分支必须一样。
当然,您可以使用没有它的语言call/cc
来实现一种语言;这是因为它处于较低的水平。您正在以特定方式翻译语言的结构,并安排此翻译,以便您可以实施call/cc
;即,通常,继续传递样式(尽管对于C中的非便携式实现,您也可以直接复制堆栈;稍后我将更深入地介绍延续传递样式)。这并没有真正提供对call/cc
本身的深入见解 - 洞察力与您使其成为可能的模型有关。最重要的是,call/cc
只是一个包装器。
现在,Haskell没有揭示延续的概念;它会破坏参考透明度,并限制可能的实施策略。 Cont
在Haskell中实现,就像所有其他monad一样,你可以把它想象成一种使用延续传递风格的延续语言模型,就像列表monad模型非确定性一样。
从技术上讲,如果您只删除callCC
和Cont
的应用,那么runCont
的定义就会输入。但是,这不会帮助您理解它在Cont
monad的上下文中是如何工作的,所以让我们来看看它的定义。 (这个定义不是当前Monad Transformer Library中使用的定义,因为它中的所有monad都是在变换器版本之上构建的,但它与snippet对Cont
的使用相匹配(仅适用于旧版本),并大大简化了事情。)
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
好的,Cont r a
只是(a -> r) -> r
,runCont
让我们从Cont r a
值中获取此功能。很简单。但它是什么意思?
Cont r a
是一个继续传递计算,最终结果为r
,结果为a
。最终结果意味着什么?好吧,让我们更明确地写出runCont
的类型:
runCont :: Cont r a -> (a -> r) -> r
所以,正如我们所看到的,"最终结果"是我们最后从runCont
获得的价值。现在,我们如何使用Cont
建立计算? monad实例具有启发性:
instance Monad (Cont r) where
return a = Cont (\k -> k a)
m >>= f = Cont (\k -> runCont m (\result -> runCont (f result) k))
嗯,好吧,如果你已经知道这意味着什么,那就很有启发性。关键是当你写Cont (\k -> ...)
时,k
计算的其余部分 - 它期望你给它一个值a
,然后将给出计算的最终结果(类型为r
,记住),然后您可以将其用作自己的返回值,因为您的返回类型也是r
。呼!当我们使用Cont
运行runCont
计算时,我们只需指定最终k
- "顶级"产生最终结果的计算结果。
这是什么"其余的计算"叫什么名字? 延续,因为它是计算的延续!
(>>=)
实际上非常简单:我们在左侧运行计算,为其提供自己的计算的其余部分。这个计算结果只是将值提供给f
,它产生自己的计算。我们运行该计算,将其提供给我们已经给出组合动作的剩余计算。通过这种方式,我们可以在Cont
:
computeFirst >>= \a ->
computeSecond >>= \b ->
return (a + b)
或者,用更熟悉的do
表示法:
do a <- computeFirst
b <- computeSecond
return (a + b)
然后我们可以使用runCont
运行这些计算 - 大部分时间,像runCont foo id
这样的工作就可以了,将foo
的结果和最终结果类型转换为wtf :: Cont String Int
wtf = Cont (\k -> "eek!")
aargh :: Cont String Int
aargh = do
a <- return 1
b <- wtf
c <- return 2
return (a + b + c)
结果
到目前为止,这么好。现在让我们感到困惑。
wtf
这里发生了什么?! Cont
是一个String
计算,最终结果为Int
,结果为Int
,但看不到aargh
。
当我们运行runCont aargh show
时会发生什么,比如说show
- 即运行计算,并Int
将其String
结果作为"eek!"
生成最后的结果?
我们回复k
。
请记住wtf
是&#34;其余的计算&#34;?我们在Cont (\k -> k 1 + k 2)
中所做的是狡猾地不调用它,而是提供我们自己的最终结果 - 然后最终结果!
这是延续可以做的第一件事。像(>>=)
之类的东西会运行剩余的计算,就像它返回1,并再次一样,它返回2,并将两个最终结果一起添加! Continuations基本上允许表达任意复杂的非本地控制流,使它们像混乱一样强大。实际上,延续是如此笼统,在某种意义上,每个monad is a special case of Cont
。实际上,您可以将(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
视为使用一种延续传递方式:
callCC
第二个参数是一个继续,它取第一个计算的结果并返回要运行的其余计算。
但我仍然没有回答这个问题:Cont
正在进行什么?好吧,它调用你给当前延续的函数。但是暂且不说,我们用Cont :: ((a -> r) -> r) -> Cont r a
callCC :: ((a -> Cont r b) -> Cont r a) -> Cont r a
做了什么呢?是的,但比较类型:
Cont
咦。你知道,r
的问题在于我们无法对我们传递的函数中 内的动作进行排序 - 我们只是生成callCC
结果以一种纯粹的方式。 Cont
允许继续访问,传递,并且通常与内部 do a <- callCC (\cc -> ...)
foo ...
计算混淆。当我们有
cc
你可以想象callCC (\cc -> ...)
是一个函数,我们可以使用函数内部的任何值调用它来使callCC
计算本身的返回值。或者,当然,我们可以正常返回一个值,但是首先调用b
会有点无意义:)
至于那里的神秘cc foo
,只是因为你可以使用callCC (\cc -> ...)
来计算你想要的任何类型,因为它转义正常的控制流程,就像我说的那样,立即使用它作为整个callCC f = Cont (\k -> runCont (f (\a -> Cont (\_ -> k a))) k)
的结果。因此,由于它永远不必实际生成一个值,因此可以返回它想要的任何类型的值。偷偷摸摸!
这将我们带到实际的实施:
k
首先,我们得到整个计算的其余部分,并将其称为f (\a -> Cont (\_ -> k a))
。但是f
部分的内容是什么?好吧,我们知道(a -> Cont r b)
的值为callCC f
,而lambda是什么 - 一个函数,它使用一个值作为Cont
的结果,并返回一个忽略其延续的k
计算,并通过callCC f
返回该值 - &#34;其余的计算&#34;从f
的角度来看。好的,因此Cont
调用的结果是另一个Cont
计算,我们需要提供延续才能运行。我们只是再次传递相同的延续,因为如果一切正常,我们希望计算返回的是我们的返回值并继续正常。 (实际上,传递另一个值不会使有意义 - 它会使用当前继续&#34;而不是&#34;除了您实际上使用&#34;。
总而言之,我希望你发现这很有启发性。延续非常强大,但是可能需要花费大量时间来确定它们的工作方式。我建议你玩cont
(你必须打电话给{{1}}才能让当前的mtl工作)并找出你如何得到你做的结果来感受控制流程。
建议继续阅读续篇:
答案 1 :(得分:10)
call/cc
是微不足道的。困难的部分是设置可能实施的环境。
我们必须首先定义一个继续传递样式(CPS)执行环境。在这种环境中,你的函数(或类似函数的东西)不会直接返回值;相反,它们被传递一个函数来表示计算中的“下一步” - “延续” - 并将它们的结果传递给那里。在Haskell中,这看起来像这样:
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
正如您所看到的,Cont
monad操作实际上是一个持续(a -> r)
的函数,将结果a
传递给continuation,并返回{的最终结果{1}},它只是传递给它的调用者(即r
monad动作应尾调用进入延续)。 Cont
只是将它从newtype包装器中取出 - 您也可以将其视为调用具有某种特定延续的动作,如runCont
中所示。
然后我们可以把它变成monad:
runCont someAction someContinuation
正如您所看到的,instance Monad (Cont r) where
return x = Cont $ \cc -> cc x
(Cont f) (>>=) g = Cont $ \cc -> f (\r -> runCont (g r) cc)
只会得到一个延续return
,并将其值传递给延续。 cc
有点棘手,它必须使用继续调用(>>=)
然后调用f
,获取操作,然后将外部延续传递给此新操作。
所以给定这个框架,继续进行很简单。我们只想调用一个连续两次的函数。棘手的部分是我们需要在一个新的monadic动作中重新包装这个延续,抛出现有的延续。所以让我们分解一下:
g
简单,不是吗?
在像Scheme这样的其他语言中,原理是相同的,尽管它可以实现为编译器原语;我们在Haskell中可以做到这一点的原因是因为继续传递是我们在Haskell中定义的,而不是在运行时的较低级别。但原理是一样的 - 你需要首先使用CPS,然后-- Invoke a raw continuation with a given argument, throwing away our normal
-- continuation
gotoContinuation :: (a -> r) -> a -> Cont r x
gotoContinuation continuation argument = Cont $ \_ -> continuation argument
-- Duplicate the current continuation; wrap one up in an easy-to-use action, and
-- the other stays the normal continuation for f
callCC f = Cont $ \cc -> runCont (f (gotoContinuation cc)) cc
是这个执行模型的一个简单应用。
答案 2 :(得分:6)
你听说过Haskell方面的一面;我会给你一个Racket / Scheme一个,无论哪个对你最有帮助,你都可以使用它。
我的回答会短得多,因为我觉得在一个简单的球拍评估器中我能给你的最佳来源是来自Shriram Krishnamurthi的PLAI,特别是第20节。我想关于包括解释器的相关部分 - 它在第205页 - 但在尝试重新格式化几次之后我决定在页面上的适当位置更有意义。
同样,我不是要在这里解释call / cc背后的想法,只是指出一个有效的实现。如果您有其他问题,请告诉我。
答案 3 :(得分:3)
冒犯 off-language 我认为在Smalltalk延续中可以实现和理解最简单的。原因是在Smalltalk中,执行堆栈由普通对象组成,可以像任何其他对象一样访问和操作。
要实现一个简单的continuation对象,需要以下两种方法。在第一种方法中,我们通过迭代父(发送者)帧(上下文)并复制它们的状态(程序计数器,临时值,参数)来初始化延续:
Continuation>>initializeFromContext: aContext
context := aContext.
stream := WriteStream on: (Array new: 200).
[ context notNil ] whileTrue: [
stream nextPut: context.
1 to: context class instSize do: [ :index |
stream nextPut: (context instVarAt: index) ].
1 to: context size do: [ :index |
stream nextPut: (context at: index) ].
context := context sender ].
values := stream contents
第二种方法是恢复执行:首先我们展开当前堆栈(再次只是执行堆栈上的一个简单循环),然后我们恢复捕获的堆栈帧,将它们重新连接到当前堆栈帧{{1使用参数thisContext
恢复执行:
anObject
使用这两种方法,我们可以轻松构建Continuation>>value: anObject
self terminate: thisContext.
stream := values readStream.
[ stream atEnd ] whileFalse: [
context := stream next.
1 to: context class instSize do: [ :index |
context instVarAt: index put: stream next ].
1 to: context size do: [ :index |
context at: index put: stream next ] ]
thisContext swapSender: values first.
^ anObject
:
callCC
这种方法的优点在于打印的代码显示了实现完整延续所需的一切(以及类似的其他类型的延续)。系统(VM)中没有隐藏任何行为。可以使用调试器逐步执行每个部分并观察执行堆栈的操作方式。
上面的代码来自Seaside网络框架。要使用代码,您可能需要使用现成的distribution并浏览到课程Continuation class>>callCC: aBlock
^ aBlock value: (self new initializeFromContext: thisContext sender)
和WAContinuation
。
答案 4 :(得分:3)
好吧,我会提供一个更短的基于Scheme的答案,因为它也被标记为“scheme”。
要了解实施call/cc
的尝试失败的原因,您必须了解continuation-passing style是什么。一旦你明白了,它就很简单了:
call/cc
无法直接实现。但是为了提供更多信息,延续传递样式是一个流控制规则,你放弃使用调用堆栈支持一个调用约定,其中每个过程调用都传递一个“额外”参数:一个被调用的闭包当程序“完成”(传递“返回值”作为参数)时,程序应该调用。这些额外的参数闭包称为 continuations 。
任何程序都可以通过一种称为 CPS转换的方式机械地转换为延续传递方式。实际上,许多Scheme系统都是这样工作的:解析程序,对其应用CPS转换,然后将CPS抽象语法树解释或转换为目标代码。
这是你在继续传递样式中实现call/cc
的方法(使用continuation
作为延续的额外参数的名称):
(define (call/cc-cps proc continuation)
(proc continuation continuation))
正如你应该看到的那样,(a)你不能以直接的方式(与CPS相反)实现这一点,并且(b)它在CPS中是微不足道的。 call/cc
只是一个过程,它将另一个过程作为其参数和(强制性)继续,并以继续作为参数和它的继续来调用该过程。