Avoiding lift with monad transformers
For all the standard mtl monads, you don't need lift
at all. get
, put
, ask
, tell
— they all work in any monad with the right transformer somewhere in the stack. The missing piece is IO
, and even there liftIO
lifts an arbitrary IO action down an arbitrary number of layers.
This is done with typeclasses for each "effect" on offer: for example, MonadState
provides get
and put
. If you want to create your own newtype
wrapper around a transformer stack, you can do deriving (..., MonadState MyState, ...)
with the GeneralizedNewtypeDeriving
extension, or roll your own instance:
instance MonadState MyState MyMonad where
get = MyMonad get
put s = MyMonad (put s)
You can use this to selectively expose or hide components of your combined transformer, by defining some instances and not others.
(You can easily extend this approach to all-new monadic effects you define yourself, by defining your own typeclass and providing boilerplate instances for the standard transformers, but all-new monads are rare; most of the time, you'll get by simply composing the standard set offered by mtl.)
You can make your functions monad-agnostic by using typeclasses instead of concrete monad stacks.
Let's say that you have this function, for example:
bangMe :: State String ()
bangMe = do
str <- get
put $ str ++ "!"
-- or just modify (++"!")
Of course, you realize that it works as a transformer as well, so one could write:
bangMe :: Monad m => StateT String m ()
However, if you have a function that uses a different stack, let's say ReaderT [String] (StateT String IO) ()
or whatever, you'll have to use the dreaded lift
function! So how is that avoided?
The trick is to make the function signature even more generic, so that it says that the State
monad can appear anywhere in the monad stack. This is done like this:
bangMe :: MonadState String m => m ()
This forces m
to be a monad that supports state (virtually) anywhere in the monad stack, and the function will thus work without lifting for any such stack.
There's one problem, though; since IO
isn't part of the mtl
, it doesn't have a transformer (e.g. IOT
) nor a handy type class per default. So what should you do when you want to lift IO actions arbitrarily?
To the rescue comes MonadIO
! It behaves almost identically to MonadState
, MonadReader
etc, the only difference being that it has a slightly different lifting mechanism. It works like this: you can take any IO
action, and use liftIO
to turn it into a monad agnostic version. So:
action :: IO ()
liftIO action :: MonadIO m => m ()
By transforming all of the monadic actions you wish to use in this way, you can intertwine monads as much as you want without any tedious lifting.