The “I’m stupid” elm language nugget #16

Recently, this question came up on the mailing list:

And I realized that there’s a way of thinking about decoders that could use elaboration.

When I make a complicated decoder, I usually start with something that uses decodeValue so I can inspect what it’s doing in the mean time:

And then I convert it to pass through decoders rather than state:

Since this is a topic that trips people up a lot (and I’ve definitely spent way too long writing json decoders in elm before), I’m going to hopefully give a thorough explanation of what I was thinking when I wrote this.

x = JE.object [(“length”, JE.int 4), (“0”, JE.string “A”), (“1”, JE.string “B”), (“3”, JE.string “C”)]

First, I made a value of the kind I wanted to decode: an array-like object with holes. In the mailing list post, HTML5 touch events are referenced, specifically TouchList, which under the surface contains numbered elements, but is officially advertised to have an item() method that retrieves Touch objects. The elm code is already getting a little sneaky here due to the lack of a call or apply Json.Decoder that would allow you to invoke a method on a javascript value (remember that the API is for Json, but the values are ordinary javascript values of any kind) (also, I really wish this existed, but kind of doubt it’d be allowed).

Next, thinking about how to get all the indices, I sketched out something that would crawl an array until a specified bound:

getUntil l res n array =
if n < l then
case Array.get n array of
Just v -> getUntil l (v :: res) (n+1) array
Nothing -> res
else
res

And went with it for the moment:

Then replaced Array.get with JD.decodeValue so it’d work on an array-like object:

getUntil decoder l res n val =
if n < l then
case JD.decodeValue (JD.field (toString n) decoder) val of
Ok v -> getUntil decoder l (v :: res) (n+1) val
Err _ -> res
else
res

(I actually tried JD.index but quickly realized that it probably won’t work on a thing that isn’t really an array).

So what we have now is a thing that, given a length will try to get the keys from an array like object and bail on the first failure. We want to skip gaps though;

getUntil decoder l res n val =
if n < l then
case JD.decodeValue (JD.maybe (JD.field (toString n) decoder)) val of
Ok v -> getUntil decoder l (v :: res) (n+1) val
Err _ -> res
else
res

It makes a list of Maybe a, which we’ll get back to.

One thing we can do from here is get a bit fancy with Result.andThen, which will help later to convert it to a proper decoder:

getUntil decoder l res n val =
if n < l then
JD.decodeValue (JD.maybe (JD.field (toString n) decoder)) val
|> Result.andThen
(\v -> getUntil decoder l (v :: res) (n+1) val)
else
Ok res

It also passed down Result String a, which is more like a decoder.

So we need the length, and we can combine the whole thing:

decodeArray decoder val =
JD.decodeValue (JD.field “length” JD.int) val
|> Result.andThen
(\len -> decoder len [] 0 val)

Yay! We get:

> x |> decodeArray JD.string
Ok ([Just “C”,Nothing,Just “B”,Just “A”])

So there’s just some cleaning to do: the values really want to be in an int map, as turning it into an array would yield a double maybe from Array.get.

decodeIndices decoder l res n val =
if n < l then
JD.decodeValue (JD.maybe (JD.field (toString n) decoder)) val
|> Result.andThen
(\v ->
let nres =
case v of
Just v -> (n,v) :: res
Nothing -> res
in
decodeIndices decoder l nres (n+1) val
)
else
Ok (Dict.fromList res)
decodeArray decoder val =
JD.decodeValue (JD.field “length” JD.int) val
|> Result.andThen
(\len -> decodeIndices decoder len [] 0 val)

I also renamed getUntil to decodeIndices because it describes what it does a bit better.

So now, we have what we want. We can try it out by wrapping it into a decoder as it is:

type alias DecFun b = JD.Value -> Result String b

packageDecoder : DecFun b -> JD.Decoder b
packageDecoder decfun =
JD.value
|> JD.andThen
(\v ->
case decfun v of
Ok x -> JD.succeed x
Err e -> JD.fail e
)

You can pack up literally anything as a json decoder, but it lacks style to a degree since it makes a lot of trips back and forth from elm to JD.decodeValue and back. Not actually tested, but I suspect it’s a bit slower too.

So then there’s one final step, converting to a decoder. Since we’re using Result.andThen in a couple of places, we can pass on the Result-ness of each stage to JD.andThen and JD.succeed:

decodeIndices : JD.Decoder a -> Int -> List (Int,a) -> Int -> JD.Decoder (Dict Int a)
decodeIndices decoder l res n =
if n < l then
JD.maybe (JD.field (toString n) decoder)
|> JD.andThen

(\v ->
let nres =
case v of
Just v -> (n,v) :: res
Nothing -> res
in
decodeIndices decoder l nres (n+1)
)
else
JD.succeed (Dict.fromList res)

decodeArray : JD.Decoder a -> JD.Decoder (Dict Int a)
decodeArray decoder =
(JD.field “length” JD.int)
|> JD.andThen
(\len -> decodeIndices decoder len [] 0)

tldr; this is how I stop panicking when I need a fancy json decoder:

  1. You can just use JD.value to yield a decoder and worry about the rest incrementally.
  2. Treat making a big fancy decoder the same way as working with ordinary values.
  3. Convert the Result-soup you wind up with to a json decoder.