Does your Monad even Lift?

Monad transformers are one of the keys to writing Haskell that solves tricker problems with interlocking effect structures. For a more complete look at monad transformers, you should take a look at Part 6 of our Monads Series. But for today we'll tackle the basic idea of "Lifting", which is one of the core ideas behind transformers.

We combine monads by placing them on a "stack". A common example might be StateT s IO. This transformer allows us to keep track of a stateful value AND perform IO actions.

basicState :: Int -> StateT Int IO Int
basicState x = do
  prev <- get
  put (2 * prev + x)
  return (x + prev)

So far, this function is only performing State actions like get and put. We'd like it to also perform IO actions like this:

basicState :: Int -> StateT Int IO Int
basicState x = do
  prev <- get
  put (2 * prev + x)
  putStrLn "Double value and added input"
  return (x + prev)

However this won't compile. The putStrLn function lives in the IO monad, and our overall expression is in the StateT monad. How do we solve this? By using lift:

basicState :: Int -> StateT Int IO Int
basicState x = do
  prev <- get
  put (2 * prev + x)
  lift $ putStrLn "Double value and added input"
  return (x + prev)

What exactly does lift do here? It tells Haskell to take a function in one monad and treat it as though it's part of another monad transformer using the original monad as its "underlying" type.

lift :: (Monad m, MonadTrans t) => m a -> t m a

To specialize it to this example:

lift :: IO a -> StateT Int IO a

Why do we call this "lifting"? This comes back to treating monads like a "stack". In this example, IO is on the bottom of the stack, and State Int is on top of it. So we have to "lift" it up one level.

Sometimes you might need to lift multiple levels! Consider this example with State, Writer, and IO. We have to use one lift for tell (the Writer function) and two for putStrLn (the IO function).

stateWriterIO :: Int -> StateT Int (WriterT [String] IO) Int
stateWriterIO x = do
  prev <- get
  put (2 * prev + x)
  lift . lift $ putStrLn "Double value and added input"
  lift $ tell ["Returning input and previous value"]
  return (x + prev)

However, with monad classes, we can sometimes skip using multiple lifts. When you combine IO with any of the basic monads, you can use the special liftIO function:

liftIO :: (MonadIO m) => IO a -> m a

stateWriterIO :: Int -> StateT Int (WriterT [String] IO) Int
stateWriterIO x = do
  prev <- get
  put (2 * prev + x)
  liftIO $ putStrLn "Double value and added input"
  lift $ tell ["Returning input and previous value"]
  return (x + prev)

Again, if you monads are a stumbling block for you, I highly encourage you to do some more in depth study and read our Monads Series in some more depth. For our next two articles, we'll go over some other useful combinators with monads in Haskell!

Previous
Previous

Cool Monad Combinators

Next
Next

Shorter Run Functions