在Scheme中执行从call/cc
获得的延续有效地跳回到初始调用/ cc并恢复保存的调用堆栈。
我刚刚开始学习Haskell,我正在试图弄清楚如何理解callCC
。在理解Scheme callCC
方面,尝试理解call/cc
。 callCC
的实施是
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
答案 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 a
和h :: a -> Cont r b
)
(类型a
的值的计算,ma,可以变成b
的值的计算 - runCont ma
给出h
,给定类型a
的值,“知道”如何生成类型b
的值的计算 - 可以使用函数f :: b -> r
来计算类型{{1}的值})
本质上,r
是h
的延续,ma
绑定>>=
及其继续生成函数的延续组合(ma
内的“隐藏”功能可生成ma
,a
内的“隐藏”功能可生成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
计算的延续内部,如果是永远都会发生。