Sometimes, you need to change the bottle depending on the content.

Juicing JSON, andThen some

A Decoder is like a box of future data. You never know what you’re gonna get.

Michel Belleville
Wat, the Elm-ist
Published in
6 min readAug 3, 2019

--

me> Back from vacations 😃 where’s my favorite flat-beaked know-it-all?

wat> Just where you left it. My vacations were nice by the way. It’s calm here without the keyboard clacking.

me> As if I know you use my rig when I’m not here to watch those kinky videos on Monads and Applicatives… 😚

wat> 😳 how do you know?

me> Apart from your know-it-all-itude on the subject? I was wondering why the almighty algorithm is suggesting me all those 40-year-old beards that talk about category theory… some are quite good you know? 😉

wat> Well, let’s see if mr wise-ass here has learned a thing or two about Monads then… what did we talk last time and is not just a Functor but also a Monad?

me> … A JSON Decoder?

wat> X-actly! 👍

me> Oooook. So, a Functor is something you can map over, right? And a Monad is something you can andThen over?

wat> Indeed. Last time we used map in one of its stranger incarnation, map3, to melt 3 different Decoder into one so that we could decode an object with 3 fields. When (and only when) all 3 fields could be correctly decoded from a JSON structure, map3 would use the function we fed it to put it all together into our desired structure. When any of the underlying decoder failed, the result would be a failure. But when all 3 succeed, you get a success with the object you want inside.

me> Yes… so, why do we need an andThen again? Aren’t all those mapN functions enough?

wat> Remember why we needed andThen on Maybe and Result?

me> So that we could not only chain operations that succeed (we do that with map) but also operations that could fail… oooh, I see what you did there 😁 A decoder can either succeed or fail, so we need a function that allows us to fail depending on the result of a previous decoder, even when that previous decoder succeeded, right?

wat> Who’s a smart boy? You the smart boy! 🍖 🐶 we might go and generalize andThen soon, just like we did with map… But first, let’s see how it works with Decoder.

Maybe.andThen : (a -> Maybe b) -> Maybe a -> Maybe b
Result.andThen : (a -> Result b) -> Result a -> Result b
-- now help yourself figure out what the signature is for...
Decoder.andThen : ?

me> Let me guess…

Decoder.andThen : (a -> Decoder b) -> Decoder a -> Decoder b

wat> In other words, give me:
- a function that turns whatever is of a type a into a Decoder that attempts to decode into something of type b
- then give me a Decoder that attempts to decode an a
- then I’ll give you a Decoder that promises to try and give you a b

me> Ok… how about we see it in action?

wat> Of course. Say I have these custom types:

type Card
= Ranked Rank Suit
| Joker
type Rank
= Two | Three | Four | Five | Six | Seven | Eight | Nine | Ten | Jack | Queen | King | Ace
type Suit
= Heart | Diamond | Club | Spade

me> Are you reusing your old types?… 😉

wat> Nothing wrong with a little recycling… now let’s say we represent the suits as simple JSON strings. We know how to decode a JSON string into a pure Elm String using the aptly-named string decoder we find in the Json.Decode module…

> decodeString string "\"Heart\""
Ok "Heart" : Result Error String

me> …but a String is not what we want, because we want our fancy Suit because we’re a fancy Elm-ist, fu-fu-fu! 😙

wat> Also, we want to fail when we can’t get a proper suit-y string. So let’s see how we can andThen our way towards a proper Decoder Suit:

suit : String -> Decoder Suit
suit perhapsSuitString =
case perhapsSuitString of
"Heart" -> Heart
"Diamond" -> Diamond
"Club" -> Club
"Spade" -> Spade

me> Wait a minute… that will not compile at all!

wat> How dare you criticize my perfect code that I did absolutely not botch on purpose to see whether you’re following?!? 😉

me> The return type of your suit function should be Decoder String, yet your case .. of operation returns a String… also, that very case .. of does not cover all possible string combinations.

wat> Yup ; let’s fix those problems:

suit : String -> Decoder Suit
suit perhapsSuitString =
case perhapsSuitString of
"Heart" -> succeed Heart
"Diamond" -> succeed Diamond
"Club" -> succeed Club
"Spade" -> succeed Spade
_ -> fail ("this is not a card suit: " ++ perhapsSuitString)

me> Let me guess… succeed is a decoder that always succeed, and fail is a decoder that always fail?

wat> Almost:

-- from Json.Decode
succeed : a -> Decoder a
fail : String -> Decoder a

me> Yeah, right, succeed is a function that takes an a and gives you a Decoder a that happens to always succeed with that very a I suppose?… as for fail… what the?! 😮 How can fail return a Decoder a? There is no a in its parameters, only in the return type! It doesn’t even know what an a is!

wat> That’s because fail never needs to know what an a is, because it will never have to give you any ; remember, a Decoder a might succeed and give you an a but it can as well fail and give you an Error. In this instance, it will just fail, wrapping the String message in its Error type to explain its failure. 🎁

me> Ooooh… smart. Now we have covered all the cases in our case .. of and we always return a Decoder Suit… Let’s use it to decode a Suit!

> decodeString (string |> andThen suit) "\"Heart\""
Ok Heart : Result Error Suit
> decodeString (string |> andThen suit) "\"not a suit\"
Err (Failure ("this is not a card suit: not a suit")) : Result Error Suit

wat> Let’s give this decoder a name:

string2Suit : Decoder Suit
string2Suit =
string |> andThen suit

wat> Now let’s assume we have a handy Decoder Rank named string2Rank

me> …since we already get for a Ranked : Rank -> Suit -> Card constructor for free, we can use map2 to do something like this:

rankedCard : Decoder Card
rankedCard =
map2
RankedCard
(field "rank" string2Rank)
(field "suit" string2Suit)
> decodeString rankedCard "{rank:\"Ace\",suit:\"Spade\"}"
Ok (RankedCard Ace Spade) : Result Card

wat> And we’ve got a Decoder Card!

me> Almost. It only decodes ranked cards, no Joker invited…

wat> 🃏Haaaa hahahahahahahahahahahaha, oh eh ah, oh eh ah… and I thought my jokes were bad… How about a magic trick?

-- also from Json.Decode
oneOf : List (Decoder a) -> Decoder a

me> So… we try a list of decoder, one after the other, and the first that succeeds gives us the value?

wat> Now what if we had a special string2joker that decodes a joker JSON string into a Joker Card value?

> decodeString string2joker "\"Joker\""
Ok Joker : Result Error Card

wat> Tadaaaaa! It’s… it’s decoded…

me> Ooooh, oooh I want to put it all together!

card : Decoder Card
card =
oneOf
[ rankedCard
, string2joker
]
> decodeString card "{rank:\"Queen\",suit:\"Heart\"}"
Ok (Ranked Queen Heart) : Result Error Card
> decodeString card "\"Joker\""
Ok Joker : Result Error Card
> decodeString card "garbage"
Err (Failure ("This is not valid JSON! Unexpected token g in JSON at position 1"))

wat> Now you’re decoding like a real pro 😄

me> Well, this little pro is going to bed… 💤

--

--