Космическая утечка в трубах с RWST

Анализ памяти из следующей программы показывает, что функции noleak работают в постоянной памяти, а функция утечки теряет память линейным образом. dflemstr указал, что это может быть связано с RWST, вызывающим бесконечную цепочку распределений. Так ли это и какие существуют другие решения? Мне действительно не нужна монада писателей.

Окружающая среда:

GHC 7.8.3 на ARCH 64 бит

ghc Pipe.hs -o Pipe -prof

import Control.Concurrent (threadDelay)
import Control.Monad (forever)

import Pipes
import Control.Monad.Trans.RWS.Strict

main = leak

effectLeak :: Effect (RWST () () () IO) ()
effectLeak =
  (forever $ do
      liftIO . threadDelay $ 10000 * 1
      yield "Space") >->
  (forever $ do
      text <- await
      yield $ text ++ (" leak" :: String)) >->
  (forever $ do
      text <- await
      liftIO . print $ text
  )

effectNoleak :: Effect IO ()
effectNoleak =
  (forever $ do
      lift . threadDelay $ 10000 * 1
      yield "Space") >->
  (forever $ do
      text <- await
      yield $ text ++ (" leak" :: String)) >->
  (forever $ do
      text <- await
      lift . print $ text
  )

leak = (\e -> runRWST e () ()) . runEffect $ effectLeak

noleak = runEffect $ effectNoleak

Ответ 1

Кажется, что Writer часть RWST на самом деле является виновником:

instance (Monoid w, Monad m) => Monad (RWST r w s m) where
    return a = RWST $ \ _ s -> return (a, s, mempty)
    m >>= k  = RWST $ \ r s -> do
        (a, s', w)  <- runRWST m r s
        (b, s'',w') <- runRWST (k a) r s'
        return (b, s'', w `mappend` w') -- mappend
    fail msg = RWST $ \ _ _ -> fail msg

Как вы можете видеть, писатель использует простой mappend. Поскольку (,,) не является строгим в своих аргументах, w `mappend` w' строит серию thunks, даже жесткий <Monoid экземпляр () довольно тривиален:

instance Monoid () where
        -- Should it be strict?
        mempty        = ()
        _ `mappend` _ = ()
        mconcat _     = ()

Чтобы исправить это, вам нужно добавить строгость к w `mappend` w' в кортеже:

        let wt = w `mappend` w'
        wt `seq` return (b, s'', wt) 

Однако, если вам не нужен Writer, вы можете просто использовать ReaderT r (StateT st m) вместо:

import Control.Monad.Trans.Reader
import Control.Monad.Trans.State.Strict

type RST r st m = ReaderT r (StateT st m)

runRST :: Monad m => RST r st m a -> r -> st -> m (a,st)
runRST rst r st = flip runStateT st . flip runReaderT r $ rst

Однако, учитывая, что это заставит вас lift вычислять правильную монаду, вы можете вместо этого использовать mtl пакет. Код останется прежним, но импорт будет в этом случае

import Control.Monad.Reader
import Control.Monad.State.Strict

Ответ 2

Зета правы, и утечка пространства происходит из-за WriterT. WriterT и RWST (как "строгие", так и ленивые версии) всегда просачиваются независимо от того, какой моноид вы используете.

Я написал более длинное объяснение этого здесь, но вот резюме: единственный способ не пропустить пространство - имитировать WriterT с помощью StateT monad где tell моделируется с использованием строгой put, например:

newtype WriterT w m a = WriterT { unWriterT :: w -> m (a, w) }

instance (Monad m, Monoid w) => Monad (WriterT w m) where
    return a = WriterT $ \w -> return (a, w)
    m >>= f  = WriterT $ \w -> do
        (a, w') <- unWriterT m w
        unWriterT (f a) w'

runWriterT :: (Monoid w) => WriterT w m a -> m (a, w)
runWriterT m = unWriterT m mempty

tell :: (Monad m, Monoid w) => w -> WriterT w m ()
tell w = WriterT $ \w' ->
    let wt = w `mappend` w'
     in wt `seq` return ((), wt)

Это в основном эквивалентно:

type WriterT = StateT

runWriterT m = runStateT m mempty

tell w = do
    w' <- get
    put $! mappend w w'