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

23/02/2023 | X min read (updated: 03/05/2023 15:22)

logo

Since the previous post had some measure of success, I decided to continue the series! ๐ŸŽ‰

Without much preamble, letโ€™s look at the typeclass definition in Haskell for Applicative Functors:

class (Functor f) => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

This looks a bit scarier ๐Ÿ‘ป at the beginning, but do not worry, we will explain every bit at a time.

The first thing we can notice is that the typeclass definition has itself a typeclass constraint! This is new for us, we did not know that could happen (until now), and this is what the class Functor f => bit means.

What are the implications of this? Well, as you might have already guessed, it just means one simple thing: every Applicative Functor must be first a valid Functor, not a very big surprise, right? ๐Ÿ˜‰

The second thing we can notice is that, contrary to the Functor typeclass declaration that specified only a function to be implemented (fmap), now we have 2 functions every Applicative Functor must have to satisfy the instance: pure, and the mysterious <*> operator, which we will call the TIE fighter operator from now on (because I love the name and yes, Iโ€™m a big STAR WARS fan ๐Ÿค“).

Letโ€™s talk about each of them separately but first, spoiler alert โš ๏ธ, in Elm, some examples of Applicative Functors you use every day are: List, Maybe, Result and Task.

The pure function

Out of the two needed functions, pure is probably the easiest to explain: it just โ€œliftsโ€ any value a into an Applicative Functor context f.

pure :: a -> f a

What are some examples of this in Elm? For example, the List.singleton function!

> List.singleton
<function> : a -> List a

Okay, what about Maybe?

> Just
<function> : a -> Maybe a

This might be new for you, but data constructors are also functions! ๐Ÿคฏ

This means that for Maybe we just have one pure function (Nothing is just a value, not a function), and you probably can guess what is the pure implementation for Result:

> Ok
<function> : value -> Result error value

This example is a little harder to understand, because the f Applicative Functor structure is actually Result error, so that this matches:

pure :: value -> f            value
Ok :    value -> Result error value

Note that, because of this, the Err contructor/function is not a correct implementation, since the Applicative Functor structure is not preserved:

> Err
<function> : error -> Result error value

You can probably see here that there is no f a structure, so only Ok could be considered the correct implementation of pure for Result.

To keep with the Elm explanations, Task.succeed is the pure equivalent for the Task Applicative Functor:

> import Task
> Task.succeed
<function> : a -> Task.Task x a

The TIE fighter operator (<*>)

Now let us begin with the fun part:

(<*>) :: f (a -> b) -> f a -> f b

This is an infix operator that takes a lifted function f (a -> b) and a lifted value f a and somehow magically applies (thatโ€™s why sometimes this function is also refered to as apply or just ap) the lifted function to the lifted a to finally return a lifted b value (f b). But by now you should be wondering: how on Earth is this actually useful!? ๐Ÿค”

Well, letโ€™s have a peek at the actual implementation of the Applicative typeclass in GHC.Base:

class (Functor f) => Applicative f where
  {-# MINIMAL pure, ((<*>) | liftA2) #-}

  -- | Lift a value.
  pure :: a -> f a

  -- | Sequential application.
  (<*>) :: f (a -> b) -> f a -> f b
  (<*>) = liftA2 id

  -- | Lift a binary function to actions.
  -- ==== __Example__
  -- >>> liftA2 (,) (Just 3) (Just 5)
  -- Just (3,5)
  liftA2 :: (a -> b -> c) -> f a -> f b -> f c
  liftA2 f x = (<*>) (fmap f x)

First thing we notice is that there is a MINIMAL pragma, this tells GHC (the main compiler of Haskell) that the required functions that a type needs to implement in order to have a valid Applicative instance are pure and <*> OR liftA2.

Second thing we can notice, is that <*> and liftA2 are almost identical: they are defined in terms of each other! ๐Ÿคฏ

So, liftA2 is basically:

liftA2 f x = (<*>) (fmap f x)

And, the TIE fighter operator is just:

(<*>) = liftA2 id

The id function is the silliest function in Haskell: id :: a -> a and in Elm is properly called identity:

> identity
<function> : a -> a

The fact that these two functions are defined in terms of each other, just means that you need to implement one of them, and you will get the other one for free. But, leaving that behind us, does not the type declaration of liftA2 look familiar to us Elm developers? ๐Ÿ‘€

liftA2 :: (a -> b -> c) -> f a -> f b -> f c

Besides, looking at the example code given, can we achieve something similar in Elm?

-- ==== __Example__
-- >>> liftA2 (,) (Just 3) (Just 5)
-- Just (3,5)

The answer is: yes, we can! ๐Ÿš€

> Maybe.map2 Tuple.pair (Just 3) (Just 5)
Just (3,5) : Maybe ( number, number1 )

Remember how I told you on the previous post that infix operators were just superior in Haskell? We are not able to do this (,) in Elm, so we need to resign ourselves to just use Tuple.pair : a -> b -> ( a, b ), which does basically the same.

If we query the Elm REPL for the type of Maybe.map2, we get:

> Maybe.map2
<function> : (a -> b -> value) -> Maybe a -> Maybe b -> Maybe value

Compare this to the type of liftA2 again:

liftA2     :: (a -> b -> value) -> f a     -> f b     -> f c
Maybe.map2  : (a -> b -> value) -> Maybe a -> Maybe b -> Maybe value

You guessed it correctly: the liftA2 equivalent in Elm are all the *.map2 functions we can find!

So, when we said before that List, Maybe, Result and Task were Applicative Functors in Elm, is because we have List.map2, Maybe.map2, Result.map2 and Task.map2.

This ties in nicely with an excellent article published by Joรซl Quenneville some time ago, called โ€œRunning Out of Mapsโ€ (very nice pun btw ๐Ÿ˜œ).

In that post, he explains that if anytime you run out of mapN functions, you can define this simple combinator:

andMap = Maybe.map2 (|>)

And if we query again the Elm REPL for the type of andMap we get the following:

> andMap = Maybe.map2 (|>)
<function> : Maybe a -> Maybe (a -> value) -> Maybe value

Am gonna casually remind you right now about the type declaration of the TIE fighter operator, in case you forgot:

(<*>) :: f (a -> b) -> f a -> f b

What can we draw from all this crazyness?

We just FOUND THE TIE FIGHTER OPERATOR IN ELM! It is just flip andMap!! ๐Ÿ˜Ž

So why all of this is important again and when the heck am I going to use Applicative Functors (I hear your mind saying ๐Ÿง ๐Ÿ’ญ)???

Well, if you have ever used the Json.Decode.Extra package, you might have probably written code like this:

decoder : Decoder Document
decoder =
    Decode.succeed Document
        |> Decode.andMap (Decode.field "id" Decode.string)
        |> Decode.andMap (Decode.field "title" Decode.string)
        |> Decode.andMap documentTypeDecoder
        |> Decode.andMap (Decode.field "ctime" Iso8601.decoder)
        |> Decode.andMap (Decode.field "mtime" Iso8601.decoder)

The exact same code in Haskell, using our beloved infix operators, would look like this:

decoder :: Decoder Document
decoder =
  Document
    <$> decodeStringField "id"
    <*> decodeStringField "title"
    <*> documentTypeDecoder
    <*> decodeIso8601Field "ctime"
    <*> decodeIso8601Field "mtime"

This means two things:

  1. You have been using Applicative Functors all along for a very long time probably without notice! ๐Ÿฅ๐Ÿฅ๐Ÿฅ
  2. Of course, this also means that Json.Decode.Decoder is also an Applicative Functor!! ๐Ÿ‘๐Ÿป

Acknowledgements

Many people has made possible the production of this blogpost, I want to personally thank Robert Pearce for his excellent Hakyll + Nix tutorial, and Domen Koลพar, for all his work with Cachix and the Nix ecosystem in general (and for his infinite patience ๐Ÿ˜‡).

I would also like to thank Chris Allen and Julie Moronukie, because together they created the Haskell Bookโ„ข๏ธ, which is still in my opinion the best possible way to learn Haskell and it is actually the reason I am today working with Haskell.

Could not be more grateful to all of them! ๐Ÿ˜

Enough bad puns for today, hope you learned something new! If you enjoyed this post and would like me to continue the series (next up would probably be MOOONAAAAAADSSSS ๐Ÿ‘ป๐Ÿฆ‡๐Ÿฆ‡๐Ÿฆ‡), please share it in your social networks and follow me on Twitter! ๐Ÿ™Œ๐Ÿป