Understand Monad Transformers(2)¶
This is a reading notes from the great book named book of monads. It helps me a lot and I quite recommend you to read it if you have any plan to read books about haskell monads.
This is the following article after understand monad transformers as there are some confusions and missing points in the previous blog.
Previous blog focused on when to use monad transformers, and how we understand the monad transformer creates a new powerful monad which supports both monadT and underlying monad features.
After several months, I would rather to say it didn't point out the core part of monad transformer, which is how the monad transformer supports the underlying monad functionalities via type class.
- what's the problem of stacking monads(the reason why we need monad transformer)
- why we can use features from both monad and monad transformer under the scope of monad transformer.
Problem of Stacking Monads¶
When writing code, you need to put the Reader
and Maybe
together to express a computation reads configuration from environment might fail. For example, you will define the code like this:
eval' :: Expr -> Reader Assignment (Maybe Int)
eval' (Op o x y) = do
u <- eval' x
v <- eval' y
case (u, v) of
(Nothing, _) -> return Nothing
(_, Nothing) -> return Nothing
(Just u', Just v') ->
case o of
Add -> return (Just (u' + v'))
Subtract -> return (Just (u' - v'))
Multiply -> return (Just (u' * v'))
Divide ->
if v' == 0
then return Nothing
else return (Just (u' `div` v'))
eval' (Literal n) = return $ Just n
eval' (Var v) = asks $ lookup v
However, to make the quick fail, you're likely to implement the Monad
typeclass for it. Then, of course you need to write a lot of dummy and tedious code to achieve your simple composition goal.
data Evaluator a = Evaluator (Reader Assignment (Maybe a))
-- the fmap applies to the result of maybe
instance Functor Evaluator where
fmap :: (a -> b) -> Evaluator a -> Evaluator b
fmap f (Evaluator ea) = Evaluator $ reader $ \assignment -> do
let d = runReader ea assignment
fmap f d
instance Applicative Evaluator where
pure :: a -> Evaluator a
pure v = Evaluator $ reader $ \_ -> return v
(<*>) :: Evaluator (a -> b) -> Evaluator a -> Evaluator b
(<*>) (Evaluator f) (Evaluator ea) = Evaluator $ reader $ \assignment -> do
let d = runReader ea assignment
let f' = runReader f assignment
f' <*> d
instance Monad Evaluator where
return :: a -> Evaluator a
return x = Evaluator $ reader $ \_ -> return x
(>>=) :: Evaluator a -> (a -> Evaluator b) -> Evaluator b
(Evaluator x) >>= f = Evaluator $ reader $ \a ->
case runReader x a of
Nothing -> Nothing
Just y ->
let Evaluator e = f y
in runReader e a
evalFail :: Evaluator a
evalFail = Evaluator $ reader $ const Nothing
After defining the monad for the stacked monads, you could define the eval''
as below, looks nice and brief comparing to the eval'
above, if we ignore the code we write to implement Evaluator
as a monad.
eval'' :: Expr -> Evaluator Int
eval'' (Op o x y) = do
u <- eval'' x
v <- eval'' y
case o of
Add -> return (u + v)
Subtract -> return (u - v)
Multiply -> return (u * v)
Divide -> if v == 0 then evalFail else return (u `div` v)
eval'' (Literal n) = return n
eval'' (Var v) = do
a <- Evaluator (asks Just)
maybe evalFail return (lookup v a)
Hand-writing these instances is definitely not the correct way, thus we need to use monad transformer instead. The monad transformer receives a monad type and then generates a new monad type which contains the features from both the provided monad and the monad transformer itself at the same layer. Hence, you needn't write any code to implement monad for Evaluator
.
New Monad Spawned by Monad Transformer¶
The monad transformer receives a monad to generate a new monad with features of them. For example, when you use ReaderT
for a Maybe
monad, the generated monad owns both features of ReaderT
and Maybe
.
Monad Transformer and Identity Monad¶
Several monads are generated by its corresponding monad transformer with monad Identity
, for example:
type State s = StateT s Identity
type Reader r = ReaderT r Identity
type Writer w = WriterT w Identity
Namely, the Identity
stands a do-nothing computation. Hence, for the monads derives from the respective monad transformer, it has the features provided by the transformer only.
MonadX Allows to Use Underlying Functionalities in MonadT¶
During the computation of MaybeT (Reader Assignment) a
, it's convenient to use both ask
feature from reader and return Nothing
from the maybe. However, let's take a further look on the details to realize the complexity hidden from the facade.
When you can ask
in the MaybeT (Reader Assignment) a
, the type of ask
looks different:
ask
inReader
:Reader r r
ask
inMaybeT
:MaybeT (Reader r) r
Even though the type system is different, but in real practice, no additional operations is needed to access the operations from all combined monads underlying. So there must be a place to do the conversion to overcome the type differences.
This is done via the type class MonadX
provided by the monad implementation(TODO: check whether it's true), where X
is the name of the monad we are abstracting over. For example, monad Reader
defines the corresponding MonadX
named MonadReader
as well to support the possible monad transformer:
class Monad m => MonadReader r m | m -> r where
-- | Retrieves the monad environment.
ask :: m r
ask = reader id
-- | Executes a computation in a modified environment.
local :: (r -> r) -- ^ The function to modify the environment.
-> m a -- ^ @Reader@ to run in the modified environment.
-> m a
The MonadX
type class asks you to implement an instance for you monad if it requires the features from X
. So if you need to implement a type class for an instance, let's say implement MonadT m
for MonadReader
, you need to do the following things. But note that this is done by the mtl
package, usually users needn't do this tedious work.
- check whether the
MonadT
instance really implements theMonadReader
, if so you needn't to much work:
instance Monad m => MonadReader r (ReaderT r m) where
ask = ReaderT return
local f m = ReaderT $ runReaderT m . f
- if the
MonadT
doesn't provide theMonadReader
, you need to pass through the layer and find it below, namely monadm
here:
instance MonadReader r m => MonadReader r (MaybeT m) where
ask = MaybeT $ runMaybeT ask
local f m = MaybeT $ runMaybeT (local f m)
At this point you have discovered how boring and tedious this matter is. You just want to use the functionalities from an underlying monad, but you need to implement its type class which is fairly simple wrapper. Luckily, this problem has been solved by the package mtl
so users won't write these kinds of instances.
In short, to make the monad transformer accesses to the underlying monad functionalities, the underlying monad must define the corresponding MonadX
for the transformer to lift. Without the MonadX
typeclass, it's impossible for monad transformer to access the underlying monad in the constructed one.
Moreover, the instances implementation is done by the mtl library instead of compiler. Usually, the instances implementation lives in the same package of the type class MonadX
. For example, MaybeT
implements an instance for MonadReader
in the Control.Monad.Reader.
instance MonadReader r m => MonadReader r (MaybeT m) where
ask = lift ask
local = mapMaybeT . local
reader = lift . reader
Conclusion of MonadX¶
The topic above explained the pain-points of the hand-writing instances during transforming, and introduced the typeclass MonadX
is defined for the monad transformer. The key points are listed below.
MonadX
is used by monad transformer to implement- the implementation is done by the package, not the compiler at the definition of typeclass
MonadX
.
Conclusion¶
This blog introduces the problems when you stacking monads together, such as you need to keep defining instances for the monads. This urges us to use monad transformer to avoid such tedious and dummy work. Then, we have learned the typeclass MonadX
helps the transformer to support the underlying monad functionalities by implements the typeclass instances for the transformer inside the mtl
package.
I believe after reading this, you can understand why the monad transformer allows you to use the functionalities from underlying monads.