Is this way to decode more than 8 fields from a JSON hash good?

I have recently added another field into one of the biggest model in my app and that took it over the 8 fields limit of the Json.Decoder.map8 function and there is no map9 available. So this took me awhile but I’ve managed to come up with this alternate approach:

decodeApply : D.Decoder a -> D.Decoder (a -> b) -> D.Decoder b
decodeApply value partial =
    D.andThen (\p -> D.map p value) partial


decodeMediaItem : D.Decoder MediaItem
decodeMediaItem =
    D.succeed MediaItem
        |> decodeApply (D.field "contentType" D.string)
        |> decodeApply (D.field "linkUrl" D.string)
        |> decodeApply (D.field "mediaUrl" D.string)
        |> decodeApply (D.field "thumbnailUrl" D.string)

This is not the big model but just to illustrate. It works with an unlimited number of fields.

So I have two questions / request for feedbacks / comments / thoughts:

  1. Is this a good idea? If not, why? Personally it feels hacky and I think it may actually have performance impact (for example, when decoding large list of objects?) but I wouldn’t know to be sure.
  2. It feels like this could have been provided as part of elm/json directly by evancz so I suppose there was a reason it wasn’t done like this at the begginging that I don’t know about?
2 Likes

The decode-pipeline library does work similarly I think. It is mentioned in the map section of the json-decode documentation as the solution for decoding more than 7 fields.

1 Like

I like this approach
https://package.elm-lang.org/packages/webbhuset/elm-json-decode/latest/

2 Likes

What you’re doing is totally normal :tada: . What you’ve just discovered is often called “pipeline-style” decoding.

Prior work

The key is the decodeApply function you wrote. It is typically implemented in terms map2 rather than andThen:

decodeApply : D.Decoder a -> D.Decoder (a -> b) -> D.Decoder b
decodeApply argDecoder functionDecoder =
  Decode.map2 (\arg function -> function arg) argDecoder functionDecoder

or more succinctly as:

decodeApply = Decode.map2 (|>)

While this function is not part of the core elm/json package, it is provided by a few third-party libraries such as:

Pipeline convenience

Looking at your example, you’ll notice that every line begins with decodeApply (D.field .... You could make a convenience function that combines both of these:

required : String -> D.Decoder a -> D.Decoder (a -> b) -> D.Decoder b
required fieldName itemDecoder functionDecoder =
  decodeApply (D.field fieldName itemDecoder) functionDecoder

this allows you to write a slightly cleaner pipeline like:

decodeMediaItem : D.Decoder MediaItem
decodeMediaItem =
  D.succeed MediaItem
    |> required "contentType" D.string
    |> required "linkUrl" D.string
    |> required "mediaUrl" D.string
    |> required "thumbnailUrl" D.string

The required function and several other pipeline helpers are provided by NoRedInk/json-decode-pipeline third-party package. This is probably the most popular approach to decoding JSON objects with more than a few fields.

General pattern

Many different data structures, both in the core Elm libraries and third party ones, provide mapN functions. Of course, they can only provide these up to some arbitrary number (typically 6 or 7). Just like for decoders, the solution when you need a larger N is to implement the equivalent to your decodeApply function (typically called andMap) for that particular data structure.

for example combining a bunch of random generators

andMap = Random.map2 (|>)

myGenerator = 
  Random.constant myFunction
    |> andMap generator1
    |> andMap generator2
    |> andMap generator3

There is an interesting symmetry between map2 and andMap. Given one you can implement the other. For example you could do:

andMap = Maybe.map2 (|>)

-- OR

map2 function maybe1 maybe2 =
  Just function
    |> andMap maybe1
    |> andMap maybe2

It turns out that in order to do this sort of multi-mapping for a data structure you need the following requirements:

  1. Some kind of constructor like Just, Json.Decode.succeed, or Random.constant
  2. One of either map2 OR andMap

All the core packages provide a constructor + map2.

While I don’t know why Evan chose not to include andMap in the core packages, it doesn’t prevent users from being able to do anything since andMap is derivable from map2. Some things we do know are that Evan tries to keep the core packages minimal, and that map2 is much easier for newcomers to grasp than andMap :slightly_smiling_face:.


Note that if you read functional programming literature, particularly Haskell-inspired literature, you may come across the term applicative. This refers to any data structure that meets the requirements listed above. For example Maybe is applicative and the Maybe.map2 implementation above could be written in Haskell as:

pure function <*> maybe1 <*> maybe2

where <*> is Haskell’s equivalent of |> andMap in Elm


10 Likes

Haha, +1 on “this is totally normal”. Your way of doing it is extremely common. Nice job re-discovering it on your own

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.