What you’re doing is totally normal
. 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:
- Some kind of constructor like
Just
, Json.Decode.succeed
, or Random.constant
- 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
.
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