Don’t forget to feed the function.

The Art of Wrapping Functions

Where we learn that andMap is not just for Decoders

Michel Belleville
Wat, the Elm-ist
Published in
5 min readApr 25, 2020

--

wat> So, that was… long. And boring. Veeery veeery boring.

me> 😅

wat> You left me to rot in a box! Again! 😡

me> But… 👉👈 I was busy with work and getting used to my ergodox layout… and tuning the layout… and getting used to the new layout, and…

wat> And you left me in that box.😠

me> … 😓 yes.

wat> Just when we were about to learn about what Applicative Functors (a.k.a Applicatives) are for. 😐

me> Oh, er, in that case, why don’t you tell us all about those very mysterious Applicatives then? 😇

wat> You’re just saying that to be nice, aren’t you?

me> No, as a matter of fact I do want to know. Last time we discovered that they were very useful to decode json in which the number of fields kept ramping up…

wat> You do remember then?… 🥺

me> Of course I do. So, what are these applicatives useful for, out of decoding?

wat> It’s all about wrapping functions in a context, so that you may give them stuff that is already wrapped in the same kind of context.

me> Er… 🤔 say again?

wat> Look, last time we had all these decoders that would, given a well-formed, fitting JSON String (or Value), extract data ; what we wanted was to bring all those data together, and we had a function that did just that, providing the data was successfully decoded.

inADecoderBindThem =
succeed oneFunctionToBringThemAll
|> andMap oneDecoderForTheElvenKingsUnderTheSky
|> andMap oneDecoderForTheDwarfLordsInTheirHallsOfStone
|> andMap oneDecoderForMortalMenDoomedToDie
|> ...

me> Sure… so, since succeed : a -> Decoder a is making a decoder that cheats and always succeeds with what we give it, regardless of how the JSON we give it to decode is structured¹. So, when we feed succeed a function, it will always succeed with that function (nothing says a type parameter like a couldn’t be a function after all).

wat> Indeed. So, in this case, we wrap our oneFunctionToBringThemAll in the “context of decoding” by putting it in a decoder that always succeeds, and then we can combine it using andMap with all those decoders that promise to give us data (provided the decoding works out fine) to make one smart decoder.

me> So far, so understood.

wat> You probably also remember that those types called Functor are in fact “things you can map over” and that those types called Monads are in fact “things you can andThen over”…

me> Sure. Then I guess an Applicative is, I don’t know, “something you can andMap over”? 😏

wat> Hey! That’s my line! But sure it is. Things like Functor and Monad (and also the number (not-exactly-a)-type in Elm) are what is called a typeclasses in Haskell (and shall not be named in Elm ; nor created by anyone outside of Elm’s core designers for now). You can see them as “clubs” for data types. If you wanna be in a typeclass, you need to agree to behave in a certain way, provide functions that implement a certain contract.

me> Let me guess… If you’re the Decoder a type and you want to be a Functor, you need to provide a map function, right?

wat> Indeed, and that map function has to respect the same pattern as all the other members of the Functor club.

-- since the map function for any functor f is:
map: (a -> b) -> f a -> f b
-- the map function when de functor is the Decoder type shall be:
map: (a -> b) -> Decoder a -> Decoder b

me> So, the andMap function for the applicative club is… what was it for Decoder again? No, don’t tell me…

andMap : Decoder (a -> b) -> Decoder a -> Decoder b-- so, I guess for any applicative function named f:
andMap: f (a -> b) -> f a -> f b

wat> Exactly! So, for andMap to work, you have to provide it with a transformation wrapped in the context of that applicative (f (a -> b)) and something of type a also wrapped in the same kind of context (f a), and what you get is something of type b wrapped in the same context.

me> So, it’s a way to work with things wrapped in a context using a function… Just like map? What’s the difference? 🤔

wat> With a simple Functor, you can take one thing wrapped in a context and apply your function on it. So, your function can only take one argument. With an Applicative, you can take a function that take as many arguments as you like and, using successive andMap calls, take many arguments wrapped in as many of the same kind context and build up to the result, still wrapped in the same kind of context.

me> So, Functor: great for working with one thing in a context, Applicative: great for working with many things wrapped in as many of the same context?

wat> That’s my human 🤗

me> But what is the context?

wat> That depends on exactly what Functor, or Applicative, or indeed Monad we’re talking about. In the case of Decoder, the context is “the (future) decoding could yield results, or fail” ; so combining multiple Decoder using andMap promises to that take all successful results and combine them using a function, or fail. Another context, like Maybe

me> Oh, I remember maybe! May I? 🤓

wat> You may.

me> Maybe is “I may have something or nothing” so andMap would look like:

andMap : Maybe (a -> b) -> Maybe a -> Maybe b

me> So andMap allows us to combine many Maybe values and, assuming none of them are Nothing, give us a Just with the combination of all those values ; or, when at least one of them was Nothing, we just get a Nothing back?

me> Exactly. So, to recap:

-- functors are things you can map over:
map : (a -> b) -> f a -> f b
-- map applies a transformation in the context of tha functor
-- changing the content but keeping the context as is
-- monads are things you can andThen over:
andThen : (a -> m b) -> m a -> m b
-- andThen applies a transformation in the context of the monad
-- with the possibility to change the structure itself
-- applicatives are things you can andMap over:
andMap : f (a -> b) -> f a -> f b
-- andMap applies a transformation that already is in the context
-- of that applicative to combine as many things that are in the
-- same context as you need

wat> And remember, a data type can be in multiple clubs at once, so Maybe, List, Decoder for example are Functors, Applicatives and Monads in their own right. In fact, you cannot be a Monad or an Applicative if you’re not already a Functor, and all Functors are also Applicatives (and vice-versa).

me> Well. I guess that’s enough mind-bending for one day…

wat> Put that box away! 😠

me> 😅 I was just putting it in its place you know.

wat> As long as you don’t put me in it again… 🙁

[1] even a succeed-generated Decoder won’t succeed when given input is not a valid JSON string

--

--