我无法理解之前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不会有问题,这是正确的吗?,但为什么呢?。
答案 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
感到困惑,这可能表明它是调用延续但实际上它正在创建延续。