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