Understand Monad Transformers¶
Monad transformer(monadT
) is another significant concept in Haskell. To understand the monadT, it's compulsory to know how monad works and the mechanism of >>=
(bind). You can view the blog Understand Monad or any popular monads to learn about them.
In this blog, I will convey how we use monad transformer while how we recognize the idea that monad transformer creates a new monad. I will explain the function lift
as well.
How We Use Monad Transformer¶
All monads are computations. Here function guardNum
is a simple computation for Maybe
monad. It returns Nothing
if the input is negative.
As time goes by, we might improve the guardNum
whereby pass the sentry value from caller instead of fixing at 0
. We can either add a new parameter or use Reader
monad to do so:
- add a new parameter
- use
Reader
monad
The problem of adding a new parameter is that the new parameter is usually passed across the sub-calls as a kind of environment for all computation. In the other word, it shouldn't be put as a function parameter.
Reader monad is convenient to carry the data among all sub-calls, while it facilitates code change in the future. Integrating the behavior of monad Reader
into the guardNum
is beneficial
Then, another problem is that we should use Reader
on the top of Maybe
, or a ReaderT
to embed them together to a new monad? The typical way is to stack the Reader
on the top of Maybe
, while it's also possible to integrate Reader
via transformer ReaderT
.
example :: Int -> Reader Int (Maybe Int)
example a = do
sentry <- ask
if a < sentry then return Nothing
else return $ Just a
guardNumWithSentry :: Int -> Int -> Maybe Int
guardNumWithSentry a sentry
| a < sentry = Nothing
| otherwise = Just a
demo :: Int -> ReaderT Int Maybe Int
demo a = do
sentry <- ask
lift $ guardNumWithSentry a sentry
For the simple example here, it doesn't matter because both of them are simple. However, we prefer the monad transformer due to its flexibility.
Then, we have a look at lift $ guardNumWithSentry a sentry
in function demo
. Because Maybe
and ReaderT Int Maybe
are not the same type, we need to convert Maybe
from the ReaderT Int Maybe
. Given the fact that Maybe
is the underlying type of the transformer, we can use lift
function to convert from the underlying type to the transformer.
Monad Transformer: Construct a More Powerful Monad¶
MonadT creates a more powerful monad which supports the all features from monadT and underlying monad. The hardest blocker to understand monadT is that how monad transformer creates a new monad that supports both monadT and monad?. I need to highlight here if you try to understand it by the wrapping way, that's a wrong direction and will make you feel quite confused.
The transformer allows the new monad supports the underlying monad because it implements its typeclass. More details are mentioned in understand monad transformer(2).
Transformer just wraps the underlying monad
Given the fact that monad transformer implements the typeclass of underlying monad, it means that the monad transformer actual works as a proxy which helps to interact between outside and the internal underlying monad. In other word, the real computation is still done by the underlying monad, and the only difference is the monad transformer hides it from users.
If you don't want to understand how the monad transformer works, from a pure user perspective, that's quite easy because the monad transformer just allows you to mix the different monads together and then use all of their features. For example, we can mix the ask
from reader and Nothing/Just
from maybe as below.
demo' :: Int -> ReaderT Int Maybe Int
demo' a = do
sentry <- ask
if a< sentry then lift Nothing
else lift $ Just a
Similar to the Reader
monad, the ask
gets the value during runReaderT
with the help of >>
. When runReaderT
is called, it will construct a reader via return
function, and then the >>
binding will propagate the data in the following ReaderT
. We can define a custom customAsk
to demonstrate it.
customAsk:: ReaderT Int Maybe Int
customAsk = ReaderT return
customAskDemo :: Int -> ReaderT Int Maybe Int
customAskDemo a = do
sentry <- customAsk
if a < sentry then lift Nothing
else lift $ Just a
main :: IO ()
main =
print (runReaderT (demo 2) 3) -- Nothing
>> print (runReaderT (demo 4) 3) -- Just 4