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
is
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:
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.

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