该示例在ContT () IO
monad 中执行,Monad 允许导致()
和一些提升的延续IO
。
type ExM a = ContT () IO a
ContT
可能是一个令人难以置信的令人困惑的单子,但我发现 Haskell 的等式推理是解开它的强大工具。该答案的其余部分分几个步骤检查原始示例,每个步骤都由句法转换和纯重命名提供支持。
所以,让我们首先检查一下callCC
部件的类型——它最终是整段代码的核心。该块负责生成一种奇怪的元组作为其一元值。
type ContAndPrev = (Int -> ExM (), Int)
getContAndPrev :: ExM ContAndPrev
getContAndPrev = callCC $ \k -> let f x = k (f, x)
in return (f, 0)
这可以通过用 分割来让它更熟悉一点(>>=)
,这正是它在真实上下文中的使用方式——任何do
-block 脱糖(>>=)
最终都会为我们创建。
withContAndPrev :: (ContAndPrev -> ExM ()) -> ExM ()
withContAndPrev go = getContAndPrev >>= go
最后我们可以检查它实际上在调用站点中的样子。为了更清楚,我将对原始示例进行一些脱糖处理
flip runContT return $ do
lift (putStrLn "alpha")
withContAndPrev $ \(k, num) -> do
lift $ putStrLn "beta"
lift $ putStrLn "gamma"
if num < 5
then k (num + 1) >> return ()
else lift $ print num
请注意,这是一个纯粹的句法转换。该代码与原始示例相同,但它突出显示了此缩进块在withContAndPrev
. 这是理解 Haskell 的秘诀callCC
---withContAndPrev
可以访问整个“do
块的其余部分”,它可以选择如何使用。
让我们忽略实际的实现,withContAndPrev
看看我们是否可以创建我们在运行示例中看到的行为。这相当棘手,但我们想要做的是将调用自身的能力传递给块。Haskell 像它一样懒惰和递归,我们可以直接写。
withContAndPrev' :: (ContAndPrev -> ExM ()) -> ExM ()
withContAndPrev' = go 0 where
go n next = next (\i -> go i next, n)
这仍然是一个令人头疼的递归问题,但可能更容易了解它是如何工作的。我们正在使用 do 块的其余部分并创建一个名为go
. 我们将一个函数传入块中,该函数go
使用一个新的整数参数调用我们的循环器,并返回前一个。
我们可以通过对原始代码进行更多语法更改来开始展开这段代码。
maybeCont :: ContAndPrev -> ExM ()
maybeCont k n | n < 5 = k (num + 1)
| otherwise = lift (print n)
bg :: ExM ()
bg = lift $ putStrLn "beta" >> putStrLn "gamma"
flip runContT return $ do
lift (putStrLn "alpha")
withContAndPrev' $ \(k, num) -> bg >> maybeCont k num
现在我们可以检查betaGam >> maybeCont k num
传入withContAndPrev
.
let go n next = next (\i -> go i next, n)
next = \(k, num) -> bg >> maybeCont k num
in
go 0 next
(\(k, num) -> betaGam >> maybeCont k num) (\i -> go i next, 0)
bg >> maybeCont (\i -> go i next) 0
bg >> (\(k, num) -> betaGam >> maybeCont k num) (\i -> go i next, 1)
bg >> bg >> maybeCont (\i -> go i next) 1
bg >> bg >> (\(k, num) -> betaGam >> maybeCont k num) (\i -> go i next, 2)
bg >> bg >> bg >> maybeCont (\i -> go i next) 2
bg >> bg >> bg >> bg >> maybeCont (\i -> go i next) 3
bg >> bg >> bg >> bg >> bg >> maybeCont (\i -> go i next) 4
bg >> bg >> bg >> bg >> bg >> bg >> maybeCont (\i -> go i next) 5
bg >> bg >> bg >> bg >> bg >> bg >> lift (print 5)
很明显,我们的假实现重新创建了原始循环的行为。我们的虚假行为是如何通过使用它作为参数接收的“do 块的其余部分”来打一个递归结来实现的,这可能会稍微清楚一些。
有了这些知识,我们就可以仔细看看callCC
。我们将通过最初以预先绑定的形式检查它来再次获利。这种形式非常简单,即使很奇怪。
withCC gen block = callCC gen >>= block
withCC gen block = block (gen block)
换句话说,我们使用callCC
, gen
, 的参数来生成 的返回值callCC
,但是我们传递到我们最终将值应用到gen
的那个延续中。block
它是递归的,但在含义上很清楚——<code>callCC 是真正的“用当前的延续调用这个块”。
withCC (\k -> let f x = k (f, x)
in return (f, 0)) next
next (let f x = next (f, x) in return (f, 0))
的实际实现细节callCC
更具挑战性,因为它们要求我们找到一种callCC
从语义中定义的方法,(callCC >>=)
但这通常是可以忽略的。归根结底,我们受益于这样一个事实,即do
写入块以便每一行都将块的其余部分绑定到它上面,(>>=)
这提供了一个自然的连续概念。