Monad Transformer Is All You Need

Abhijit Gupta
The Startup
Published in
5 min readJul 26, 2020

--

Have your Monads shake hands with each other

Let me put it succinctly — monad transformers helps us combine operations of several monads into one single monad. Well, that is it. If it were as simple as said, you would not have a plethora of articles about monads and monads transformers, with each one claiming to simplify and explain what these things are in a better way. I won’t make any such claim. Understanding monads and transformers require time and one needs to see several practical examples to appreciate these abstract concepts which have their origin in category theory. I will, instead, show you a few useful strategies where the use of monad transformers provides us with a solution to the monad composition problem.

Functors and Applicatives both possess the closure property under composition — you can compose two functors or two applicatives and expect to get another functor or applicative, as the case may be. This does not always hold true for monads. With that being said, we also do not have a generic recipe that we can employ every time for building monad transformers. In such a scenario, a carefully planned strategy often helps in designing “good” monad transformers.

Let’s start by reviewing a few basic concepts intrinsic in the understanding of monad transformers.

The Monad class defines two basic operators: (>>=) (bind) and return

class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a

The bind operator (>>=) has the type — (>>=) :: Monad m => m a -> (a -> m b) -> m b . It takes monadic a, a function that transforms from type a to monadic b, and gives us the result of type monadic b. For instance, Just 5 >>= (\x -> Just (x + 3)) gives back result of type Maybe, i.e., Just 8

If we carefully look at the bind operator, what we see is that it is an fmap followed by join.

m >>= f = join $ fmap f m 

If you have already begun scratching your head, wondering what these terms are, hold on a little! Introducing fmap and join is a necessary digression for appreciating the subtlety and core logic behind monads transformers.

fmap is intrinsic to a Functor.

class Functor f where    fmap :: (a -> b) -> f a -> f b

fmap preserves structure — that is to say — If a type is an instance of Functor, you can use fmap to apply a function to values in it. Transforming a functor instance with an identity function returns the same functor instance. In modern C++, you can express this as

fmap | transform ([] auto value) { return value;}) == fmap

Second key property is composability — transforming a functor with f and then with g is he same as transforming the functor with the composition of those f and g g.f

fmap | transform (f) | transform (g) == 
fmap | transform ([=] (auto value) { return g(f (value)); })

I won’t be covering C++ implementation in detail, but I hope you get the idea.

Now back to businees, join has the type join :: Monad m => m (m a) -> ma . It is available in Control.Monad module. It “peels off” one level of monadic structure, projecting its bound argument into the outer level. As you may have guessed by its signature, it is used to join different monadic contexts together.

List type in Haskell is a Functor, for which fmap and map are equivalent. Let’s try applying the above definition of bind operator using list as an example.

>join $ fmap (\x -> [x]) [1,2,3]
[1,2,3]

Doesn’t sound interesting. The above operation is equivalent to identity. It gives the original list back. Let’s try something else —

>join $ fmap (\x -> [x]) [[1],[1,2]]
[[1],[1,2]]
> join $ fmap (\x -> x) [[1],[1,2]]
[1,1,2]

Here, join and fmap are compatible so we get the expected results. However, when we try to apply join on something incompatible, the computation fails. join will not work unless the adjacent monads match!

> join $ fmap (\x -> Just x) [1,2,3]
FAILS!

Often we want to compose two such monadic types together. This brings us to monads transformers.

Imagine you are given data of the type (Num a) => [Maybe a] [Just 12, Just 1771, Just 13, …] and you intend to add k to every value. We cannot use applicative <*> , owing to type incompatibility. Employing monad transformer is a viable solution.

runMaybeT $ (MaybeT  $ fmap (\x -> Just x) [1..100]) >>= (\x -> MaybeT $ [Just (x+k)])

fmap (\x -> Just x) [1..100] is used to generate our input data. It gives us [Just 1, Just 2, …, Just 100]. MaybeT is our monad transformer. Its type signature is MaybeT :: m (Maybe a) -> MaybeT m a . Applying it on our input, we get data of MaybeT [] a type. Now, a is of type (Num p) => p . We now apply MaybeT on our wrapped function (\x -> MaybeT $ [Just (x+k)]) This will take a value of (Num p) => p type and return a monad of type MaybeT [] a . If you have been following closely, you know now we can apply bind operator (>>=) , as they have compatible types. Application of bind operator, gives us the desired result. All that is left is unwrapping the output for recovering our initial ordinary type. We use runMaybeT for this purpose. runMaybeT :: MaybeT m a -> m (Maybe a)

This works fine for recovering the non-transformer variant.

As mentioned earlier, when we apply a monad transformer on a normal monad, the result is another monad. We can chain several monad transformers on top of each other to give a stack of monad transformers, and in fact it’s common in practice. This short piece is in no way a comprehensive beginner’s guide to monads or monads transformers. To understand and appreciate them fully, you may need to solve several real world problems that involve composing monads together. For further understanding, a digression into realms of category theory is certainly helpful.

--

--