了解Haskell callCC示例

时间:2013-12-12 06:42:44

标签: haskell continuations callcc

我无法理解之前question的答案。我希望对以下内容的解释将澄清事情。以下示例来自fpcomplete

import Control.Monad.Trans.Class
import Control.Monad.Trans.Cont

main = flip runContT return $ do
    lift $ putStrLn "alpha"
    (k, num) <- callCC $ \k -> let f x = k (f, x)
                               in return (f, 0)
    lift $ putStrLn "beta"
    lift $ putStrLn "gamma"
    if num < 5
        then k (num + 1) >> return ()
        else lift $ print num

输出

alpha
beta
gamma
beta
gamma
beta
gamma
beta
gamma
beta
gamma
beta
gamma
5

我想我理解这个例子是如何工作的,但为什么有必要在let中使用callCC表达式来回复&#34;延续,以便以后可以使用。因此,我尝试通过以下更简单的示例并修改它来直接返回延续。

import Control.Monad.Trans.Class
import Control.Monad.Trans.Cont

main = flip runContT return $ do
    lift $ putStrLn "alpha"
    callCC $ \k -> do
      k ()
      lift $ putStrLn "uh oh..."
    lift $ putStrLn "beta"
    lift $ putStrLn "gamma"

打印

alpha
beta
gamma

我将其修改为以下

import Control.Monad.Trans.Class
import Control.Monad.Trans.Cont

main = flip runContT return $ do
    lift $ putStrLn "alpha"
    f <- callCC $ \k -> do
      lift $ putStrLn "uh oh..."
      return k
    lift $ putStrLn "beta"
    lift $ putStrLn "gamma"

这个想法是延续会以f的形式返回,并且在我希望打印的测试示例中未被使用

uh oh...
beta
gamma

但是这个例子没有编译,为什么不能这样做呢?

编辑:考虑Scheme中的分析示例。据我所知,Scheme不会有问题,这是正确的吗?,但为什么呢?。

4 个答案:

答案 0 :(得分:2)

反向顺序查看您的示例。

由于无限类型,最后一个示例没有进行类型检查。查看callCC的类型,它是((a -> ContT r m b) -> ContT r m a) -> ContT r m a。如果我们尝试返回延续,我们返回ContT r m (a -> ContT r m b)类型的东西。这意味着我们得到类型相等约束a ~ (a -> ContT r m b),这意味着a必须是无限类型。 Haskell不允许这些(一般来说,有充分的理由 - 据我所知,这里的无限类型将是一个沿线的东西,为它提供一个无限顺序函数作为参数。)

你没有提到在第二个例子中是否有任何你感到困惑的东西,但是。它不打印“呃哦......”的原因是因为ContT产生的k ()动作与许多ContT动作不同,使用以下计算。这是继续和正常函数之间的区别,它返回ContT个动作(免责声明,任何函数可以返回ContT这样的动作,但一般来说)。因此,当您使用打印件或其他任何内容跟随k ()时,这是无关紧要的,因为k ()只会放弃以下操作。

所以,第一个例子。这里的let绑定实际上只用于将参数弄乱到k。但通过这样做,我们避免了无限类型。实际上,我们在let绑定中做了一些递归,这与我们之前获得的无限类型有关。 f有点像已经完成递归的延续版本。

我们传递给callCC的这个lambda的类型是Num n => ((n -> ContT r m b, n) -> ContT r m b) -> ContT r m (n -> ContT r m b, n)。这与你上一个例子没有相同的无限类型问题,因为我们搞乱了参数。您可以通过使用let绑定以其他方式添加额外参数来执行类似的技巧。例如:

recur :: Monad m => ContT r m (ContT r m ())
recur = callCC $ \k -> let r = k r in r >> return r

这可能不是一个非常好解释的答案,但基本的想法是直接返回延续会产生无限类型的问题。通过使用let绑定在传递给callCC的lambda中创建一些递归,你可以避免这种情况。

答案 1 :(得分:2)

正如其他人写的那样,最后一个例子由于无限类型而没有进行类型检查。

@augustss提出了解决这个问题的另一种方法:

  

您还可以创建一个newtype以将无限(等)递归类型包装到(iso-)递归newtype中。 - 奥古斯特于2013年12月12日12:50

以下是我的看法:

import Control.Monad.Trans.Cont
import Control.Monad.Trans.Class

data Mu t = In { out :: t (Mu t) }

newtype C' b a = C' { unC' :: a -> b }
type C b = Mu (C' b)

unfold = unC' . out
fold = In . C'

setjmp = callCC $ (\c -> return $ fold c)
jump l = unfold l l

test :: ContT () IO ()
test = do
    lift $ putStrLn "Start"
    l <- setjmp
    lift $ putStrLn "x"
    jump l

main = runContT test return

我认为这是@augustss的想法。

答案 2 :(得分:1)

该示例在ContT () IO monad中执行,Monad允许继续导致()和一些提升IO

type ExM a = ContT () IO a

ContT可能是一个令人难以置信的混淆monad工作,但我发现Haskell的等式推理是解开它的强大工具。本答案的其余部分分几个步骤检查原始示例,每个步骤由句法变换和纯重命名提供。

所以,让我们首先检查callCC部分的类型 - 它最终是整段代码的核心。该块负责生成一种奇怪的元组作为其monadic值。

type ContAndPrev = (Int -> ExM (), Int)

getContAndPrev :: ExM ContAndPrev
getContAndPrev = callCC $ \k -> let f x = k (f, x) 
                                in return (f, 0)

通过使用(>>=)对其进行分区可以更加熟悉这一点,这正是它在真实环境中的使用方式 - 任何do - 块desugaring将创建{{1}对我们来说最终。

(>>=)

最后我们可以检查它在呼叫站点中的实际情况。为了更清楚,我会稍微贬低原来的例子

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 下此缩进块的存在。这是理解Haskell的秘诀withContAndPrev --- callCC可以访问整个“withContAndPrev块的其余部分”,然后选择如何使用。

让我们忽略do的实际实现,看看我们是否可以创建运行示例时看到的行为。这是相当棘手的,但我们想要做的是将块自身调用的能力传递给块。 Haskell像它一样懒惰和递归,我们可以直接写出来。

withContAndPrev

这仍然是递归头痛的问题,但可能更容易看出它是如何工作的。我们将使用do块的其余部分并创建一个名为withContAndPrev' :: (ContAndPrev -> ExM ()) -> ExM () withContAndPrev' = go 0 where go n next = next (\i -> go i next, n) 的循环结构。我们将一个函数调用我们的looper 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

很明显,我们的假实现重新创建了原始循环的行为。通过使用作为参数接收的“do block的其余部分”绑定递归结,可能会更清楚我们的假行为是如何实现的。

有了这些知识,我们可以仔细研究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) 。我们将通过最初以预先约束的形式检查它来再次获利。这种形式非常简单,如果很奇怪的话。

callCC

换句话说,我们使用withCC gen block = callCC gen >>= block withCC gen block = block (gen block) callCC的参数来生成gen的返回值,但我们将callCC传递给了gen。 1}}我们最终将值应用于。这是递归的,但是从表面上看是明确的 - block真的是“用当前的延续来称呼这个块”。

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绑定的块的剩余部分,这提供了一个自然的连续概念。< / p>

答案 3 :(得分:0)

  

为什么有必要在callCC中使用let表达式来“返回”   延续以便以后可以使用

确切地使用continuation,即捕获当前的执行上下文,然后使用此捕获延续来跳回到该执行上下文。

您似乎对功能名称callCC感到困惑,这可能表明它是调用延续但实际上它正在创建延续。