Search code examples
haskellhaskell-pipes

Space leak in Pipes with RWST


A memory analysis of the following program shows that the noleak functions runs in constant memory while the leak function leaks memory in a linear fashion. dflemstr indicated that this might be due to RWST causing an infinite chain of allocations. Is this the case and what other solutions exists? I actually dont need the Writer monad.

Environment:

GHC 7.8.3 on ARCH 64 bit

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

Solution

  • It seems like the Writer part of RWST is actually the culprit:

    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
    

    As you can see, the writer uses a plain mappend. Since (,,) isn't strict in its arguments, w `mappend` w' builds a series of thunks, even tough the Monoid instance of () is rather trivial:

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

    In order to fix this, you need to add strictness to w `mappend` w' in the tuple:

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

    However, if you don't need the Writer anyway, you can simply use ReaderT r (StateT st m) instead:

    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
    

    However, given that this will force you to lift the computations to the correct monad, you might want to use the mtl package instead. The code will stay the same, but the imports will be the following in this case

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