Monad transformer

This week, let’s talk about Haskell. I has been learning Haskell in my free time for a while and i am really enjoy it. I recommend everybody to learn it, whether you can use it for your day job or not, because it will give you a very different view and way of thinking to improve your programming skills.

Let’s start with a very simple problem likes this: given a user id, we want to find all posts of that user.

type Post = String
type UserId = String
data User = User { _userId :: UserId }

findUser :: String -> IO (Maybe User)
findUser = undefined

findPost :: User -> IO (Maybe [Post])
findPost = undefined

getPosts :: UserId -> IO (Maybe [Post])
getPosts userId = do
  maybeUser <- findUser userId
  case maybeUser of
    Just user -> findPost user
    Nothing -> return Nothing

First, we find the user by calling findUser. If we find the user, we will call findPost to get all posts of that user. Notice the implementation of getPosts, we need to unwrap Maybe, pattern match on it.

In case you are new to Haskell, IO is the monad that allows you do IO side effect in Haskell, read file, print to console, make network, api call. Maybe is another monad to represent computation that might go wrong by not returning a value. So when you see Maybe User, it means this function may return a user, may be not. That’s why we need to check if the Maybe value is Just or Nothing. And IO (Maybe User) means this function will make some IO call to outside, can read a file, query database and returns a Maybe User.

Let’s have another example to see the problem more clearly. Now we has another requirement, only friends can see all posts of each other. So we need to check friend relationship before return all posts.

isFriend :: User -> User -> IO (Maybe Bool)
isFriend = undefined

getFriendPosts :: UserId -> UserId -> IO (Maybe [Post])
getFriendPosts userId friendId = do
  maybeUser <- findUser userId
  case maybeUser of
    Just user -> do
      maybeFriendUser <- findUser friendId
      case maybeFriendUser of
        Just friendUser -> do
          maybeIsFriend <- getRelationShip user friendUser
          case maybeIsFriend of
            -- I really want to give up this blog post at this point.
            Just isFriend -> do
              if isFriend
              then 
                getPosts friendId
              else
                return Nothing
            Nothing -> return Nothing
        Nothing -> return Nothing
    Nothing -> return Nothing

Now, the code looks really ugly now. We have a deep nested code, each step we need to manual unwrap Maybe value, process if it has value, otherwise return Nothing. This looks really bad not only because of nested steps, but also because we can’t leverage Maybe monad feature.

Let’s recall the Monad instance of Maybe and IO.

instance Monad Maybe where
  Nothing >>= f = Nothing
  Just x >> f = f x

-- This is not the real implementation of IO monad but you can get the idea
instance Monad IO where
  (action1 >>= action2) world0 =
    let (a, world1) = action1 world0
        (b, world2) = action2 a world1
    in (b, world2)

In Maybe monad, the computation chains will stop when Nothing is returned from any computation, so Nothing >>= f will return Nothing. In IO monad, it will run each computation sequencely. So if we mix them together, our IO Maybe mond which behave like IO normal monad. But in this case, we want our IO Maybe to also have behaviour of Maybe monad. If any computation returns Nothing, it should stop and return Nothing.

do notation is a syntax sugar in haskell

run = do
  runA
  runB

is

run = runA >>= \_ -> runB

So the main problem here is we use two monad IO and Maybe together, and right now they don’t work well with each other. We only use feature of IO monad and not Maybe monad. To illustrate the idea, here is what will happens if we write our code with only Maybe monad.

type Post = String
type UserId = String
data User = User { _userId :: UserId }

findUser :: String -> Maybe User
findUser = undefined

findPost :: User -> Maybe [Post]
findPost = undefined

getPosts :: UserId -> Maybe [Post]
getPosts userId = do
  user <- findUser userId
  findPost user

isFriend :: User -> User -> Maybe Bool
isFriend = undefined

getFriendPosts :: UserId -> UserId -> Maybe [Post]
getFriendPosts userId friendId = do
  user <- findUser userId
  friend <- findUser friendId
  friendCheck <- isFriend user friend
  if friendCheck
  then
    getPosts friendId
  else
    Nothing

It looks nicer, right? We don’t need to check for Just and Nothing at every step, we depend on the monad instance of Maybe to take care of it.

If only we can make our IO Maybe behave like both IO, which can make IO call to outside world, and Maybe, represent a computation may return nothing and stop as soon as one function returns Nothing, we will be able to make the code above less verbose and more expressive. Let’s try to create that new monad together, we will call it MaybeIO monad.

newtype MaybeIO a = MaybeIO { runMaybeIO :: IO(Maybe a) }

findUser :: String -> MaybeIO User
findUser = undefined

findPost :: User -> MaybeIO [Post]
findPost = undefined

isFriend :: User -> User -> MaybeIO Bool
isFriend = undefined

Now before we rewrite getFriendPosts, we need to provide monad instance for our MaybeIO.

instance Functor MaybeIO where
  fmap f = MaybeIO . fmap (fmap f) . runMaybeIO

instance Applicative MaybeIO where
  pure a = MaybeIO $ return (Just a)
  f <*> a = MaybeIO $ (<*>) <$> (runMaybeIO f) <*> (runMaybeIO a)

instance Monad MaybeIO where
  return a = MaybeIO $ return (Just a)

  x >>= f = MaybeIO $ runMaybeIO x >>= \a -> case a of
      Just value -> runMaybeIO $ f value
      Nothing -> return Nothing

Basically remember our MaybeIO is actually IO Maybe, so we write our new monad based on functionality of these two. For the (>>=) function, we called runMaybeIO x, which return us a IO Maybe. Then we use (>>=) instance of IO to get back a, which is Maybe monad.

If you confuse because of runMaybeIO and MaybeIO constructor, here are their signatures:

MaybeIO :: IO (Maybe a) -> MaybeIO a
runMaybeIO :: MaybeIO a -> IO (Maybe a)

We use them to convert between our new MaybeIO monad and the original IO Maybe.

Now it’s time to rewrite our getFriendPosts

getFriendPosts :: UserId -> UserId -> MaybeIO [Post]
getFriendPosts userId friendId = do
  user <- findUser' userId
  friend <- findUser' friendId
  friendCheck <- isFriend' user friend
  if friendCheck
  then
    getPosts' friendId
  else
    return mempty

It looks like the example with Maybe monad above, right? We’ve just combined IO and Maybe together into a single monad, which behave like Maybe and have IO capability. If we go one step further, generalize it, instead of MaybeIO, we want to use Maybe with any other monad, add Maybe behaviour to it.

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

instance Monad m => Functor (MaybeT m) where
  fmap f = MaybeT . fmap (fmap f) . runMaybeT

instance Monad m => Applicative (MaybeT m) where
  pure a = MaybeT $ return (Just a)
  f <*> a = MaybeT $ (<*>) <$> (runMaybeT f) <*> (runMaybeT a)

instance Monad m => Monad (MaybeT m) where
  return a = MaybeT $ return (Just a)

  x >>= f = MaybeT $ runMaybeT x >>= \a -> case a of
      Just value -> runMaybeT $ f value
      Nothing -> return Nothing

Instead of MaybeIO, we have MaybeT m with m is another monad such as IO. Our MaybeIO above become MaybeT IO. We have finished written our first Monad Transformer, MaybeT.

Monad Transformer allows us to stack multiple monads together and behave likes one single monad, combined all functionalities of these monads inside the stack. For example, if we want to build a real application, we need to read global config (Reader monad), write log (Writer monad), store application state (State monad), handling exception (Either monad), and of course IO monad. We can write a ReaderT AppConfig (WriterT String (StateT AppState (ExceptT String IO))) monad to represent our application and reuse all functionality of these monads.

That’s all for this post. We saw two real world problems. We learn to write a custom MaybeIO monad which combined Maybe and IO together to solve the problem. After that we generalize it into MaybeT monad transformer and learn how monad transformer can help us in real application by stack multiple monad together.

Next time, we will look at mtl and transformers, two monad transformer libraries in Haskell ecosystem, which provide us the transformer version of all standard monads so we don’t need to write our own. We will also look at the order of the monad in the stack and how it will effect behaviour of the whole stack.

Thank you.

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.