Haskell for Elm developers: giving names to stuff (Part 2 - Applicative Functors)
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
= (<*>) (fmap f x) liftA2 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:
= (<*>) (fmap f x) liftA2 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:
- You have been using Applicative Functors all along for a very long time probably without notice! ๐ฅ๐ฅ๐ฅ
- 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! ๐๐ป