36

这个线程 (Control.Monad.Cont fun, 2005),Tomasz Zielonka 引入了一个函数(Thomas Jäger 以清晰而优美的方式评论)。Tomasz 接受 callCC 主体的参数(一个函数)并将其返回以供以后使用,具有以下两个定义:

import Control.Monad.Cont
...
getCC :: MonadCont m => m (m a)
getCC = callCC (\c -> let x = c x in return x)

getCC' :: MonadCont m => a -> m (a, a -> m b)
getCC' x0 = callCC (\c -> let f x = c (x, f) in return (x0, f))

Haskellwiki中也提到了这些。使用它们,您可以类似于 haskell 中的 goto 语义,看起来非常酷:

import Control.Monad.Cont

getCC' :: MonadCont m => a -> m (a, a -> m b)
getCC' x0 = callCC (\c -> let f x = c (x, f) in return (x0, f))

main :: IO ()
main = (`runContT` return) $ do
    (x, loopBack) <- getCC' 0
    lift (print x)
    when (x < 10) (loopBack (x + 1))
    lift (putStrLn "finish")

这将打印数字 0 到 10。

有趣的地方来了。我将它与 Writer Monad 一起使用来解决某个问题。我的代码如下所示:

{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, UndecidableInstances #-}

import Control.Monad.Cont
import Control.Monad.Writer

getCC :: MonadCont m => m (m a)
getCC = callCC (\c -> let x = c x in return x)

getCC' :: MonadCont m => a -> m (a, a -> m b)
getCC' x0 = callCC (\c -> let f x = c (x, f) in return (x0, f))

-- a simple monad transformer stack involving MonadCont and MonadWriter
type APP= WriterT [String] (ContT () IO)

runAPP :: APP a -> IO ()
runAPP a= runContT (runWriterT a) process
      where process (_,w)= do
               putStrLn $ unlines w
               return ()

driver :: Int -> APP ()
driver k = do
   tell [ "The quick brown fox ..." ]
   (x,loop) <- getCC' 0
   collect x
   when (x<k) $ loop (x+1) 

collect :: Int -> APP ()
collect n= tell [ (show n) ] 

main :: IO ()
main = do
   runAPP $ driver 4

当您编译并运行此代码时,输​​出为:

The quick brown fox ...
4

数字 0 到 3 在这个例子的深邃黑暗中被吞没了。

现在,在“Real World Haskell”中,奥沙利文、戈尔岑和斯图尔特状态

“堆叠 monad 转换器类似于组合函数。如果我们改变应用函数的顺序,然后得到不同的结果,我们不会感到惊讶。monad 转换器也是如此。” (真实世界的 Haskell,2008 年,第 442 页)

我想出了交换上面的变压器的想法:

--replace in the above example
type APP= ContT () (WriterT [String] IO)
...
runAPP a = do
    (_,w) <- runWriterT $ runContT a (return . const ())
    putStrLn $ unlines w

但是,这不会编译,因为在 Control.Monad.Cont 中没有 MonadWriter 的实例定义(这就是我最近问这个问题的原因。)

我们添加一个实例,离开监听并传递未定义:

instance (MonadWriter w m) => MonadWriter w (ContT r m) where
  tell = lift . tell
  listen = undefined
  pass = undefined

添加这些行,编译并运行。所有数字都打印出来。

前面的例子发生了什么?

4

2 回答 2

33

这是一个有点非正式的答案,但希望有用。 getCC'返回到当前执行点的延续;您可以将其视为保存堆栈帧。返回的延续getCC'不仅有ContT' 在调用点的状态,ContT还有堆栈上任何 monad 的状态。当您通过调用延续来恢复该状态时,上面构建的所有 monad 都会ContT返回到调用点的状态getCC'

在第一个示例中,您使用type APP= WriterT [String] (ContT () IO), withIO作为基础 monad,然后ContT和 finally WriterT。因此,当您调用 时loop,writer 的状态将恢复为调用时的状态,getCC'因为 writerContT在 monad 堆栈上。当您切换ContTandWriterT时,现在延续只会展开ContTmonad,因为它高于 writer。

ContT不是唯一会导致此类问题的 monad 转换器。这是一个类似情况的例子ErrorT

func :: Int -> WriterT [String] (ErrorT String IO) Int
func x = do
  liftIO $ print "start loop"
  tell [show x]
  if x < 4 then func (x+1)
    else throwError "aborted..."

*Main> runErrorT $ runWriterT $ func 0
"start loop"
"start loop"
"start loop"
"start loop"
"start loop"
Left "aborted..."

即使 writer monad 被告知值,但在ErrorT运行内部 monad 时它们都被丢弃了。但是如果我们切换变压器的顺序:

switch :: Int -> ErrorT String (WriterT [String] IO) () 
switch x = do
  liftIO $ print "start loop"
  tell [show x]
  if x < 4 then switch (x+1)
    else throwError "aborted..."

*Main> runWriterT $ runErrorT $ switch 0
"start loop"
"start loop"
"start loop"
"start loop"
"start loop"
(Left "aborted...",["0","1","2","3","4"])

在这里,writer monad 的内部状态被保留,因为它低于ErrorTmonad 堆栈。ErrorT和之间的最大区别ContT是 thatErrorT的类型清楚地表明,如果抛出错误,任何部分计算都将被丢弃。

推断它ContT何时位于堆栈顶部肯定更简单,但有时能够将 monad 展开到已知点很有用。例如,可以以这种方式实现一种交易。

于 2011-03-05T11:46:22.880 回答
11

我花了一些时间在 λ 演算中追踪这一点。它生成了一页又一页的推导,我不会在这里尝试重现,但我确实对 monad 堆栈的工作原理有了一些了解。您的类型扩展如下:

type APP a = WriterT [String] (ContT () IO) a
           = ContT () IO (a,[String])
           = ((a,[String]) -> IO()) -> IO()

您可以类似地扩展 Writer 的return,>>=tell, 以及 Cont 的return, >>=, 和callCC。但是追踪它是非常乏味的。

调用loop驱动程序的效果是放弃正常的继续,而是再次从调用返回getCC'。被放弃的延续包含将当前添加x到列表中的代码。所以相反,我们重复循环,但现在x是下一个数字,只有当我们达到最后一个数字(因此不要["The quick brown fox"]放弃继续)时,我们才会将列表从and拼凑起来["4"]

正如“Real World Haskell”强调 IO monad 需要保持在堆栈底部一样,continuation monad 保持在顶部似乎也很重要。

于 2011-03-04T23:55:17.690 回答