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!