call / cc实现?

时间:2012-01-29 03:39:03

标签: haskell scheme continuations callcc

我正在尝试查找call / cc的实现方式。我发现的最好的是这个Haskell片段:

callCC f = Cont $ \k -> runCont (f (\a -> Cont $ \_ -> k a)) k

虽然由于ContrunCont而不是我想要的那么简单。我也找到了它的功能描述,尽管从未像实际代码那样清晰。

那么它是如何以最简单的形式实现的?我用Scheme和Haskell标记这个,因为这是我喜欢的两种语言。

5 个答案:

答案 0 :(得分:80)

"实施call/cc"在你工作的那个层面上真的没有意义;如果你能用一种语言实现call/cc,那就意味着它有一个内置的构造,至少和call/cc一样强大。在语言本身的层面上,call/cc基本上是一个原始的控制流操作符,就像某种形式的分支必须一样。

当然,您可以使用没有它的语言call/cc来实现一种语言;这是因为它处于较低的水平。您正在以特定方式翻译语言的结构,并安排此翻译,以便您可以实施call/cc;即,通常,继续传递样式(尽管对于C中的非便携式实现,您也可以直接复制堆栈;稍后我将更深入地介绍延续传递样式)。这并没有真正提供对call/cc 本身的深入见解 - 洞察力与您使其成为可能的模型有关。最重要的是,call/cc只是一个包装器。

现在,Haskell没有揭示延续的概念;它会破坏参考透明度,并限制可能的实施策略。 Cont在Haskell中实现,就像所有其他monad一样,你可以把它想象成一种使用延续传递风格的延续语言模型,就像列表monad模型非确定性一样。

从技术上讲,如果您只删除callCCCont的应用,那么runCont的定义就会输入。但是,这不会帮助您理解它在Cont monad的上下文中是如何工作的,所以让我们来看看它的定义。 (这个定义不是当前Monad Transformer Library中使用的定义,因为它中的所有monad都是在变换器版本之上构建的,但它与snippet对Cont的使用相匹配(仅适用于旧版本),并大大简化了事情。)

newtype Cont r a = Cont { runCont :: (a -> r) -> r }

好的,Cont r a只是(a -> r) -> rrunCont让我们从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)

实施{p> 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只是一个过程,它将另一个过程作为其参数和(强制性)继续,并以继续作为参数和它的继续来调用该过程。