Haskell for Elm developers: giving names to stuff (Part 3 - Monads!)

01/03/2023 | X min read (updated: 03/03/2023 12:57)

logo

It is finally time, I did not think I would ever write a Monad tutorial, but here it is! 😅 Let us have a look at the way Monads are defined in Haskell:

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

The first thing we can notice again is that the typeclass definition has itself a typeclass constraint, implied by the class Applicative m => bit, just like in our previous post about Applicative Functors.

Needless to say, this means that every Monad instance must satisfy the Applicative instance first, and that one, in return, the Functor instance. 😵‍💫 By the way, if you have not already, to understand this whole post and gain more intuition about Monads, you better read the previous two posts I made:

  1. Haskell for Elm developers: giving names to stuff (Part 1 - Functors)
  2. Haskell for Elm developers: giving names to stuff (Part 2 - Applicative Functors)

SPOILER ALERT ⚠️! In Elm, some examples of Monads you use every day are… 🥁🥁🥁 (drum rolls…) again: List, Maybe, Result and Task!!! 🤯

The return function

If you have good memory, you might be asking yourself right now: what is the difference between pure :: a -> m a and return?

return :: a -> m a

And the answer is: there is none.

If you want to find out the historical reasons why we ended up with two functions called different that basically do the same thing, please check this Reddit thread from 7 years go. 👴🏻 There is a modern Haskell trend to prefer pure over return in code (I contributed to this trend a fair bit myself), but it is totally up to you!

The Mr. Pointy (🤣) operator (>>)

I did not come up with the name, I swear, I read it in the Haskell Book™️. Some people refer to it as the sequencing operator, but it does not have an “official” English-language name:

(>>) :: m a -> m b -> m b

The only thing the Mr. Pointy operator does is sequencing two actions while discarding any result value of the first action.

Let’s have another peek at the implementation of the Monad typeclass in GHC.Base and see what we can learn this time from it:

class (Applicative m) => Monad m where
  -- | Sequentially compose two actions, passing any value produced
  -- by the first as an argument to the second.
  (>>=) :: m a -> (a -> m b) -> m b

  -- | Sequentially compose two actions, discarding any value produced
  -- by the first, like sequencing operators (such as the semicolon)
  -- in imperative languages.
  (>>) :: m a -> m b -> m b
  m >> k = m >>= \_ -> k
  {-# INLINE (>>) #-}

  -- | Inject a value into the monadic type.
  return :: a -> m a
  return = pure

First thing we can notice is hey! Another language pragma, this time called INLINE pragma, do not worry too much about it, all it is doing is performing a little optimization on the compiler level to tell GHC that it can go ahead and inline that function. A second thing we can notice is that Mr. Pointy (>>) is defined in terms of >>=:

  (>>) :: m a -> m b -> m b
  m >> k = m >>= \_ -> k

Because of this, we are not going to waste any time trying to find the definition of Mr. Pointy in Elm (because if you define >>= for your type, you again get for free the definition of >>), but rather cut right to the meat!

The Monadic bind operator (>>=)

I am going to try my best to give you the simplest explanation about this operator, but since I assume you know Elm, the explanation is going to be quite trivial: 😉

(>>=) :: m a -> (a -> m b) -> m b

By looking at the type signature of the bind operator, does it not look familiar to you, dear Elm developer? Have you ever tried to flatMap a List? flatMap is the name chosen by JavaScript and many other languages, but Elm chose a couple of interesting ones, let us begin first with the List Monad. 🙊

> List.concatMap
<function> : (a -> List b) -> List a -> List b

As you can see, List.concatMap is just a flipped version of the >>= operator for Elm, but what about Maybe, Result and Task?

> Maybe.andThen
<function> : (a -> Maybe b) -> Maybe a -> Maybe b

Yes! We can say with confidence that those types satisfy the Monad instance because we have Maybe.andThen, Result.andThen and Task.andThen and all of those functions allow us to chain computations! 👏🏻 Hope that was not so scary after all! 👻😘

The infamous do notation

Famously, the issue with the |> andThen approach, is that elm-format (the de facto standard for all Elm applications) formats everything in a rather ugly manner:

map5 :
    (a -> b -> c -> d -> e -> result)
    -> Task x a
    -> Task x b
    -> Task x c
    -> Task x d
    -> Task x e
    -> Task x result
map5 func taskA taskB taskC taskD taskE =
    taskA
        |> andThen
            (\a ->
                taskB
                    |> andThen
                        (\b ->
                            taskC
                                |> andThen
                                    (\c ->
                                        taskD
                                            |> andThen
                                                (\d ->
                                                    taskE
                                                        |> andThen (\e -> succeed (func a b c d e))
                                                )
                                    )
                        )
            )

This, however, is not an issue that elm-format is responsible for (as a matter of fact, I love elm-format and think it is a modern masterpiece of software engineering! 😍), but it is rather a consequence of the language design decision NOT to have something like do notation in Elm. For example, the above code would be really similar without do notation in Haskell (if you use a formatter like ormolu):

map5 ::
  (a -> b -> c -> d -> e -> result) ->
  Task x a ->
  Task x b ->
  Task x c ->
  Task x d ->
  Task x e ->
  Task x result
map5 func taskA taskB taskC taskD taskE =
  taskA
    >>= \a ->
      taskB
        >>= \b ->
          taskC
            >>= \c ->
              taskD
                >>= \d ->
                  taskE
                    >>= \e -> pure (func a b c d e)

Yes, there are Haskellers that still to this very day format their code by hand (😅), and so they would use less indentation in the aforementioned code, but we are not gonna let humans get in the way of the machine (thank God we have formatters 😍). Nevertheless, thanks to do notation, we can write it in the following manner:

map5 ::
  (a -> b -> c -> d -> e -> result) ->
  Task x a ->
  Task x b ->
  Task x c ->
  Task x d ->
  Task x e ->
  Task x result
map5 func taskA taskB taskC taskD taskE = do
  a <- taskA
  b <- taskB
  c <- taskC
  d <- taskD
  e <- taskE
  pure $ func a b c d e

Doesn’t it just look beautiful!? 💜

Funnily enough, ormolu is the closest we can get to something like elm-format in Haskell (which I also love 🤩 and am using to format every code sample in this blogpost) and many Haskellers hate it for no reason at all! 🤷🏼‍♂️

EDIT: As pointed out by @TankorSmash on Twitter, the actual closest to elm-format would probably be hindent-elm, but it might not work 100% correctly or as complete as other Haskell formatters.

Acknowledgements

Special thanks to @forensor and other readers that have encouraged me to continue the series. Kudos to Aaron VonderHaar and to Mark Karpov for creating and maintaining elm-format and ormolu respectively! 🙌🏻

Thanks again to @serras for technical proofreading this post again (he single-handedly wrote an entire book exclusively about Monads after all 🫡) and remember! Always be nice to each other online and have in mind that we are all in different learning paths in our lives and that we can help each other out by giving constructive feedback, rather than trying to destroy people’s hopes and dreams. 😅

Hope Monads finally clicked for you ✨ (if they had not already) and you learned something new! If you enjoyed this post and would like me to continue the series (next up would probably be maybe parser combinators?, let me know what you would like to hear next!), please share it in your social networks and follow me on Twitter! 🙌🏻