了解Haskell callCC示例

use*_*220 5 continuations haskell callcc

我无法理解上一个问题的答案.我希望以下的解释能够澄清事情.以下示例来自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
Run Code Online (Sandbox Code Playgroud)

输出是

alpha
beta
gamma
beta
gamma
beta
gamma
beta
gamma
beta
gamma
beta
gamma
5
Run Code Online (Sandbox Code Playgroud)

我想我理解这个例子是如何工作的,但是为什么有必要letcallCC"返回"连续中使用一个表达式,以便以后可以使用它.所以我尝试通过以下更简单的示例并修改它来直接返回延续.

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"
Run Code Online (Sandbox Code Playgroud)

这打印

alpha
beta
gamma
Run Code Online (Sandbox Code Playgroud)

我将其修改为以下内容

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"
Run Code Online (Sandbox Code Playgroud)

这个想法是继续将f在我希望打印的测试示例中返回并且未被使用

uh oh...
beta
gamma
Run Code Online (Sandbox Code Playgroud)

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

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

esa*_*981 5

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

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

您还可以创建一个新类型来将无限(等)递归类型包装成(等)递归新类型。– 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
Run Code Online (Sandbox Code Playgroud)

我认为这就是@augustss 的想法。


小智 4

反过来看你的例子

由于无限类型,最后一个示例不进行类型检查。看类型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 ()不同,使用以下计算。这是延续函数和返回操作的普通函数之间的区别(免责声明,任何函数都可以返回这样的操作,但一般来说)。因此,当您通过打印或其他任何内容跟进时,这是无关紧要的,因为它只是丢弃以下操作。ContTContTContTk ()k ()

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

我们传递给的这个 lambda 的类型callCCNum 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
Run Code Online (Sandbox Code Playgroud)

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