Juicing JSON, andThen some
A Decoder
is like a box of future data. You never know what you’re gonna get.
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?
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
| Jokertype Rank
= Two | Three | Four | Five | Six | Seven | Eight | Nine | Ten | Jack | Queen | King | Acetype 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 😄