unendlich - Home About Archive

Monad Transformers finally clicked

Posted on 20.07.2016

I have had an interest in Haskell for a long time. When I first encountered the language, resources such as Real World Haskell and Learn You a Haskell didn’t exist yet, so I tried to make do with the A “Gentle” Introduction to Haskell, which had me scratching my head a lot. At that time I even started writing a tool in Haskell to help my father run his business. But then other things came up and I was left with that superficial knowledge of Haskell that I always wanted to deepen but never got to.

A couple of months ago I stumbled on some praise online for a new Haskell book, which promised to teach everything from first principles. When I saw that the book included chapters on more recent topics such as Foldable and Traversable and on things I never properly got such as the Reader monad and monad transformers, I decided to give it a try. I was not disappointed. One of the many a-ha moments I had reading the book was about those famous monad transformers.

I had an ingrained notion that I brought from other languages that Haskell data types were just either enumerations or data containers. For instance:

-- enumeration
data Bool = False | True

-- container
data BinTree a = Nil | Node a (BinTree a) (BinTree a)

Therefore I was puzzled when reading code examples given in the book that seemed to just be about pointless wrapping and unwrapping of data with a MaybeT monad transformer. Then I realized that this wrapping and unwrapping was being done in order to have access to the Monad instance of MaybeT and use its instance functions against the wrapped data. In other words, types were being used not to hold other data, as in a collection, but to change the behaviour of that same data.

A small example should demonstrate why this is useful. Let’s start a module:

module MaybeT where

import Control.Monad.Trans.Maybe

In this example, there are two text files with key-value pairs. The first one is a dictionary from Portuguese to English:

casa house
carro car
mala suitcase
mesa table

and the second one is a price list:

house 1000000
chair 2300
suitcase 600
pen 15

These data sources could be replaced by a database or a web service and the reasoning would be the same. In order to access those sources I/O needs to be performed and therefore the result will be embedded in the IO type. Moreover, an optional result type (Maybe) should be used in case we search for a non-existant key. Therefore the functions that retrieve data will be:

mkTable f = let mkPair (x:y:_) = (x, f y) in
            fmap (mkPair . words) . filter (/= "") . lines

getItem :: String -> (String -> a) -> String -> IO (Maybe a)
getItem file f key = readFile file >>= return . lookup key . mkTable f

getTranslation :: String -> IO (Maybe String)
getTranslation = getItem "dict.txt" id

getPrice :: String -> IO (Maybe Int)
getPrice = getItem "prices.txt" read

We want to, given a product named in Portuguese, to have its price. The types of the results of those functions show that to access the data we want, we need to unwrap first the IO type and then Maybe. An attempt would be:

v1 :: String -> IO (Maybe Int)
v1 key = do
  item <- getTranslation key
  case item of
    Nothing -> return Nothing
    Just x -> getPrice x

The do notation unwraps the IO monad, and the Maybe is unwrapped with a case statement. Running this in ghci:

Prelude> :l MaybeT.hs
*MaybeT> v1 "livro"
Nothing
*MaybeT> v1 "casa"
Just 1000000
*MaybeT>

It was necessary to, in this example, manually unwrap the Maybe type embedded in IO so as to call getPrice on the result. Although in this case the effort was not very big, things can escalate quickly with more functions.

Let’s see a possible definition of MaybeT:

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

This declaration shows that a single object named runMaybeT is being held with the type m (Maybe a). It is the composition of a type m we know nothing about, besides the fact it has also an instance of Monad, and the Maybe type we know, which is wrapped by m. So the MaybeT transformer is a way of accessing the innermost data bypassing the outer monad, which we don’t care about, without needing to manually unwrap it at every step, as the next example show:

v2 :: String -> IO (Maybe Int)
v2 key = runMaybeT $ do
  item <- MaybeT $ getTranslation key
  MaybeT $ getPrice item

It is not terribly shorter than the first version, but the advantages are clear when the number of steps increases. For each new function added to the chain a case statement would have to be added to the first version, while in the second version the code is shorter and the flow is obvious. Besides, newtype is a restricted version of data that doesn’t exist at runtime: All this wrapping and unwrapping of MaybeT seen in the code above will be elided by the compiler. Other monad transformers like EitherT, ReaderT, StateT, etc. work in similarly, they let you abstract away the outer monad and work on the data you care about.

I am glad I bought this book. This insight alone made it worth for me.