Dealing with monad transformers

Michael Snoyman edited this page Jan 20, 2014 · 4 revisions
Clone this wiki locally

NOTE: Since conduit 1.0.11, the monad transformer situation is much improved due to the Data.Conduit.Lift module. For more information, see the announcement blog post.

conduit provides the transPipe function in order to allow pipes with different underlying monads to interact. However, some of the semantics of this function are surprising. In particular, it will run the monad modification function multiple times. This means that if you use transPipe to unwrap a monad, side effects will be thrown away. Perhaps an example will demonstrate best:

{-# LANGUAGE OverloadedStrings #-}
import Data.Conduit
import qualified Data.Conduit.List as CL
import Control.Monad.State
import Control.Monad.Trans.Class

source :: Source IO ()
source = CL.sourceList $ replicate 10 ()

replaceNum :: Conduit () (StateT Int IO) Int
replaceNum = awaitForever $ \() -> do
    i <- lift get
    lift $ put $ i + 1
    yield i

main :: IO ()
main = do
    x <- source $$ transPipe (flip evalStateT 1) replaceNum =$ CL.consume
    print x

    y <- flip evalStateT 1
       $ transPipe lift source $$ replaceNum =$ CL.consume
    print y

The output from this program is:


What's happening in the first case is that evalStateT is run anew for each time the Pipe is called, and the results are then thrown away. Therefore, each time the pipe is run, it is run with a state of 1.

In the second case, we only run the state once, and therefore the state is properly threaded and side effects retained between runs. To get the types to work, we lift the monad in the source instead. Since the lift call does not discard side effects, all side effects are retained.

In other words:

  • You can always use transPipe with functions which do not discard data. Examples are lift and runReaderT.
  • It's best to lift functions to higher monads than to constantly unwrap.
  • If you must unwrap and need to retain side effects, you can try such tricks as putting a mutable variable in a ReaderT instead of using StateT.