如何解释Haskell中的callCC?

时间:2013-12-08 07:39:51

标签: haskell scheme continuations callcc

在Scheme中执行从call/cc获得的延续有效地跳回到初始调用/ cc并恢复保存的调用堆栈。

我刚刚开始学习Haskell,我正在试图弄清楚如何理解callCC。在理解Scheme callCC方面,尝试理解call/cccallCC的实施是

callCC f = cont $ \h -> runCont (f (\a -> cont $ \_ -> h a)) h

据我所知,没有提到有关保存或恢复调用堆栈的内容。如何解释Haskell中的callCC来自熟悉Scheme的call/cc

编辑:也许有人可以将以下内容翻译成Haskell,这有助于我理解。

(define (f return)
  (return 2)
  3)

(display (f (lambda (x) x))) ; displays 3

(display (call-with-current-continuation f)) ; displays 2

2 个答案:

答案 0 :(得分:7)

要理解callCC在Haskell中的含义,你可能想要查看它的类型,而不是它的实现:

callCC :: MonadCont m => ((a -> m b) -> m a) -> m a

这里第一个也是最重要的是MonadCont m。这意味着callCC仅适用于实现MonadCont的monad - 这可能会令你失望,但IO不是MonadCont的实例。在这方面,callCC不像它在方案中那样工作。

无论如何,callCC的参数是((a -> m b) -> m a):这是一个将(a -> m b)作为参数的计算,这是callCC正在捕获的延续。所以让我们尝试编写一些使用callCC的东西:

import Control.Monad.Cont
fun _ = return "hi"
main = print $ runCont (callCC fun) id

现在这很无聊,因为我们不以任何方式使用延续。让我们尝试不同的乐趣:

fun' escape = do escape "ahoy"
                 return "die die die"

当你运行代码时,你会发现逃避的“调用”永远不会“返回” - 它已经像在方案中那样调用了延续。您可能知道“返回”在Haskell中不起作用:这不是短路操作。您可以将callCC视为为您提供一种提前终止计算的方法。

在实现级别上,cont和runCont是转换为continuation-passing-style的函数。您将需要更详细地研究continuation monad,以了解它是如何工作的。试试例如。 http://www.haskellforall.com/2012/12/the-continuation-monad.html

(在许多方案实现中,call / cc实际上并不是通过“保存调用堆栈”来工作。如果你将程序转换为CPS,那么调用/ cc类型就会“免费”掉出来。我您可能想要阅读例如。http://www.pipeline.com/~hbaker1/CheneyMTA.html,其中讨论了CPS可以在较低级别实施的一种方式。)

答案 1 :(得分:3)

它与Scheme的call / cc非常相似。你需要考虑它是在Cont monad。

使用ContT定义实际功能。 ContT是一个monad转换器,允许将continuation添加到其他monad中,但让我们先看看它如何与Identity monad一起使用,并将自己限制为Cont。

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

此处,Cont r a表示可以计算a类型的某个值的函数,因为给定类型为a->r的函数,它可以计算类型r的值。

这显然是一个单子:

return x = Cont $ \f -> f x

(类型a的值的一个微不足道的“计算”)

ma >>= h = Cont $ \f -> runCont ma $ \a -> runCont (h a) f

(此处ma :: Cont r ah :: a -> Cont r b

(类型a的值的计算,ma,可以变成b的值的计算 - runCont ma给出h,给定类型a的值,“知道”如何生成类型b的值的计算 - 可以使用函数f :: b -> r来计算类型{{1}的值})

本质上,rh延续ma绑定>>=及其继续生成函数的延续组合(ma内的“隐藏”功能可生成maa内的“隐藏”功能可生成h)。这是你正在寻找的“堆栈”。

让我们从简化类型开始(不使用b):

ContT

这里,callCC使用一个函数来构造一个连续的延续。

有一点很重要,你似乎也缺席了。 callCC :: ((a -> Cont r b) -> Cont r a) -> Cont r a 只有在callCC之后有继续时才有意义 - 即有继续通过。让我们考虑它是callCC - 块的最后一行,它与使用do必须包含某些内容相同:

>>=

会做的。这里重要的一点是,当您看到callCC f >>= return "blah" 的左侧,当您看到它时,可以更容易理解callCC的操作。

了解>>=的工作原理,并考虑>>=的正确关联性,您可以看到>>=中的h实际上是使用当前延续构建的 - 它使用callCC f = cont $ \h -> runCont (f (\a -> cont $ \_ -> h a)) h右侧显示的h - 整个>>= - 从callCC后面的行到结尾来构建:

do

你可以在这里看到(callCC f) >>= h = Cont $ \g -> runCont (cont $ \h -> runCont (f (\a -> cont $ \_ -> h a)) h) $ \a -> runCont (h a) g = [reduction step: runCont (Cont x) => x] Cont $ \g -> (\h -> runCont (f (\a -> Cont $ \_ -> h a)) h) $ \a -> runCont (h a) g = [(\h -> f) (\a -> ...) => f [h/(\a -> ...)] -- replace occurrences of h with (\a -> ...)] Cont $ \g -> runCont (f (\a -> Cont $ \_ -> (\b -> runCont (h b) g) a)) $ \a -> runCont (h a) g = [(\b -> runCont (h b) g) a => runCont (h a) g] Cont $ \g -> runCont (f (\a -> Cont $ \_ -> runCont (h a) g)) $ \a -> runCont (h a) g 本质上如何忽略传递给\_ -> runCont (h a) g的函数调用之后的延续 - 并“切换堆栈”,切换到当前的延续f调用h的地方。

(如果callCC是链中的最后一个,则可以应用类似的推理,尽管在这种情况下“当前延续”是传递给callCC的函数不太清楚

最后一点是runCont并没有真正使用最后runCont (f...) h,如果h的实际调用发生在h计算的延续内部,如果是永远都会发生。