100 args for the function in the box, put a value in, pass it around, 99 args for the functions in the box…

One field too many

Sometimes you can’t find a mapN with a high enough N

Michel Belleville
Wat, the Elm-ist
Published in
5 min readNov 25, 2019

--

me> Aaaaargh, not again!

wat> I hate the sound of frustration in the morning. What’s wrong?

me> What’s wrong? I’ll tell you what’s wrong! That is what’s wrong:

type alias Person =
{ firstName: String
, lastName: String
, age: Int
, email: String
, phone: String
, hairColor: String
, eyeColor: String
, favoriteFood: String
, favoriteDrink: String
}

wat> That looks like a very fine type alias (if a bit on the wide side). What about it?

me> It used to have two less fields (favoriteFood and favoriteDrink are late additions) so when I wanted to decode it from JSON, I could just use Decode.map7 and be done with it:

type alias Person =
{ firstName: String
, lastName: String
, age: Int
, email: String
, phone: String
, hairColor: String
, eyeColor: String
}
personDecoder : Decoder Person
personDecoder =
Decode.map7 Person
(field "firstName" string)
(field "lastName" string)
(field "age" int)
(field "email" string)
(field "phone" string)
(field "hairColor" string)
(field "eyeColor" string)

wat> So it was. Now you need a map9 I guess…

me> …which, as it happens, does not exist. Or at least not in Json.Decode.

wat> Well, some people¹ will tell you that this is a design problem, and that you should be using a nested structure instead, grouping fields who go together and putting them in smaller boxes that you can put in a bigger box, etc.

me> Well, sure, but… isn't there a… functor-y way to do it without having to refactor all my code just to make space for the new fields?…

wat> Are you somehow trying to manipulate me into revealing more forbidden, esoteric knowledge of deep, dark computer science?

me> …yes?

wat> Well, it's… working obviously. Here, take a look at this intriguing new function:

-- (from Json.Decode.Extra)
andMap : Decoder a -> Decoder (a -> b) -> Decoder b

me> Wait a minute… don’t I know this from somewhere?… The name is kinda familiar…

wat> You might find it looks kind of like Json.Decode.map if you switched the parameters (and squinted quite a bit):

-- (from Json.Decode.Extra)
andMap : Decoder a -> Decoder (a -> b) -> Decoder b
-- (from Json.Decode)
map : (a -> b) -> Decoder a -> Decoder b

me> Yeah… but it’s as if we swapped the transformation function with the decoder… and wrapped it in a Decoder for some reason. Why would we wrap a function inside a Decoder?

wat> Let's say for now this is not a function. Let's say it's an "empty b" that only needs an a to be complete.

me> Hmm… ok. So, it's a constructor of kinds?

wat> Sure. Now let's say that andMap is like a robotic arm that can pull an a from a Decoder a and put that a inside our "empty b" to make a complete b.

me> Got it.

wat> Let’s see it in action with the easiest of examples, a decoder that takes a string field and feed it to a very simple person:

type alias Person =
{ firstName: String }
personDecoder : Decoder Person
personDecoder =
andMap
(field "firstName" string) -- : Decoder String
(succeed Person) -- : Decoder (String -> Person)

me> That sounds… uselessly complicated. But yes, you take a string decoder that extracts the string from the firstName field, andMap somehow unwraps it and then feeds it to the Person constructor in its always-succeeding decoder. And I guess when the Decoder String fails, the Decoder Person we get is just a failing decoder. I guess I get it.

wat> Now let’s write it in a more pipeline-y way:

personDecoder =
succeed Person
|> andMap (field "firstName" string)

me> Ok, that’s the same thing only we feed the constructor in its always-succeeding decoder to andMap using (|>). So what?

wat> Now let’s add another field to Person, that also needs decoding:

type alias Person =
{ firstName: String
, lastName: String
}
personDecoder : Decoder Person
personDecoder =
succeed Person
|> andMap (field "firstName" string)
|> andMap (field "lastName" string)

wat> No mapN required 😉

me> Oh. It's like your "robotic arms" are chained together to form some kind of assembly line, putting the elements one by one in our constructor… Brilliant!

wat> Yes! Let’s return to our definition of andMap shall we?

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

wat> So, we have this Person : String -> String -> Person constructor that wants two strings to build its Person value, right? And we have two decoders that can decode the strings. But those decoders may fail. So we first wrap the constructor into a successful Decoder, and we use andMap to combine them with the decoding operation:

succeed Person -- this is a Decoder (String -> String -> Person)
|> andMap (field "firstName" string)
-- the result is a Decoder (String -> Person)
-- (the first String is provided by the first decoder)
|> andMap (field "lastName" string)
-- the result is a Decoder Person
-- (now it can decode a whole Person :)

me> Let me try and…

type alias Person =
{ firstName: String
, lastName: String
, age: Int
, email: String
, phone: String
, hairColor: String
, eyeColor: String
, favoriteFood: String
, favoriteDrink: String
}
personDecoder : Decoder Person
personDecoder =
succeed Person
|> andMap (field "firstName" string)
|> andMap (field "lastName" string)
|> andMap (field "age" int)
|> andMap (field "email" string)
|> andMap (field "phone" string)
|> andMap (field "hairColor" string)
|> andMap (field "eyeColor" string)
|> andMap (field "favoriteFood" string)
|> andMap (field "favoriteDrink" string)

me> 😢 this is so beautiful…

wat> And you know what?… Just like map is the Functor’s super-important function, and andThen the Monad’s, there is a name for things that you can andMap over…

me> Later. Let me bask in the feeling.

wat> …and that name is Applicative Functor. We’ll try and generalize soon. Bye.

[1] do not listen to these people ; your big flat objects are fine if you think they're fine ; you should group when it makes sense to you, not to please someone else. Also, those people tend to be the same that find your objects too deeply nested anyways.

--

--