Originally for Elm v0.17. Updated on for Elm v0.18.
One of the first things you will notice is Json.Decode.map and Json.Decode.map8, and the numbers between. Soon you will ask: how do decode structures larger than map8âwhere is map9, map10, and, more importantly, mapN? The documentation will lead you to elm-decode-pipeline which is not inherently bad, but hides the underlying concept from users with a DSL.
In Elm, Json.Decode.Decoder is an applicative functorâa fancy functional programming term that weâll attempt to demystify. By leveraging the power of map and apply, we should be able to wrangle even the toughest JSON thrown our way. Iâll be the first to admit that initially I found JSON decoding incredibly confusingâliterally to the point where I abandoned some projects early on because I didnât know how to decode some scary JSON.
What weâre building towards:
If you want to follow along and run the code for yourself
This install Elm, its dependencies, and start up Elm Reactor for you to point your browser at http://localhost:8000.
git clone https://github.com/toastal/elm-applicatives-and-json-decoders.git
cd elm-applicatives-and-the-json-decoders
npm install
npm startQuick Fly-By at Applicatives via Maybe
So even the most novice Elm developer knows how Maybe worksâitâs a Just a or Nothing.
To go from a Just 1 to a Just 3 weâd use map because Maybe is a functor.
Maybe.map ((+) 2) (Just 1) == Just 3
--=> TrueThe a in Just a can also be a function.
So what happens if we had a Just (+) with the addition infix operator⊠how do we use this to add in an applicative manner to add Just 1 and Just 2?
Applicatives have 2 terms:
pure,singletonapply,ap,<*>
Pure
pure is a function from something a that creates a singleton list for the default case of an applicative of the same type a (which is why it sometimes goes by the name singleton). But to make it more clear, letâs look at the type signature of pure in Haskell.
pure :: Applicative f => a -> f aKnowing that, letâs look at some examples of Elm singletons:
Maybe
Just : a -> Maybe aResult
Ok : a -> Result x aSet
singleton : comparable -> Set comparableList
flip (::) [] : a -> List aApply
Next we peek at the ability to lift values in an applicative with apply (Haskell):
liftA :: Applicative f => (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f bSo what is exactly is Just (+)?
foo : Maybe (number -> number -> number)
foo =
Just (+)Looking at the type signature, itâs a Maybe holding the addition function, (+).
What is Maybe.Extra.andMap?
andMap : Maybe (a -> b) -> Maybe a -> Maybe bThat looks an awful lot like apply/lift⊠So letâs use it:
import Maybe.Extra as Maybe
-- foo = Just (+)
bar : Maybe (number -> number)
bar =
foo |> Maybe.andMap Just 1And letâs apply values to completion
baz : Maybe number
baz =
bar |> Maybe.andMap Just 2
isJust3 : Bool
isJust3 =
baz == Just 3
--=> TrueAnd the same thing, creating an infix for andMap/apply:
import Maybe.Extra as Maybe
singleton : a -> Maybe a
singleton =
Just
infixl 2 <*>
(<*>) : Maybe (a -> b) -> Maybe a -> Maybe b
(<*>) =
flip Maybe.andMap
isJust3 : Bool
isJust3 =
(singleton (+) <*> Just 1 <*> Just 2) == Just 3
--=> True
isNothing : Bool
isNothing =
(singleton (+) <*> Just 1 <*> Nothing) == Nothing
--=> True
isAlsoNothing : Bool
isAlsoNothing =
(singleton (+) <*> Nothing <*> Just 2) == NothingLook at the the demo MaybeApplicative.elm.
So where have we seen something like this?
-- given the function fooâŠ
foo : number -> number -> number
foo x y =
x * y
-- partially apply in a 1
foo' : number -> number
foo' =
foo 1
-- Easter Egg: weâve created a monoid
foo' 37 == 37
--=> TrueThe (space) operator is function application ;)
So how does this relate to Json Decoders?
Looking in the docs for Json.Decode we have succeed:
succeed : a -> Decoder aLooks pretty pure and singleton-y to meâŠ
And in Json.Decode.Extra we have andMap and its flipped infix |:
andMap : Decoder a -> Decoder (a -> b) -> Decoder b
(|:) : Decoder (a -> b) -> Decoder a -> Decoder bWell thatâs obviously the apply functionâŠ
So letâs apply (heh heh) our knowledge
Weâll start by creating some hand-rolled, artisnal JSON:
coolJson : String
coolJson =
"""
[
{
"foo": 0,
"bar": true
},
{
"foo": 1,
"bar": true
},
{
"foo": 2,
"bar": false
}
]
"""Create some type alias to represent these cool data items
type alias CoolItem =
{ foo : Int
, bar : Bool
}We are also going to need to know about field (whose old infix was := prior to Elm v0.18) with the signature String -> Decoder a -> Decoder a
So field is used to apply the given decoder given a string for a key in a JSON object (e.g. âfooâ will be decoded as an integer).
import Json.Decode as Decode
fooDecoder : Decoder Int
fooDecoder =
Decode.field "foo" Decode.intNow that weâre set up, letâs create a CoolItem decoder using applicative-style
A reminder about Elm types, CoolItem can be used as a constructor for our CoolItem, which is this case is CoolItem : (Int -> Bool -> CoolItem).
So what happens when when apply in our foo decoder?
Letâs look at some type signatures and find out:
import Json.Decode as Decode exposing (Decoder)
import Json.Docode.Extra as Decode exposing ((|:))
-- Creating out Decoder our CoolItem constructor
baz : Decoder (Int -> Bool -> CoolItem)
baz =
Decode.succeed CoolItem
-- When we apply a decoder for our first property, foo => Int
qux : Decoder (Bool -> CoolItem)
qux =
Decode.succeed CoolItem
|: Decode.field "foo" Decode.int
-- And the full decoder, which completes the CoolItem constructor
coolItemDecoder : Decoder CoolItem
coolItemDecoder =
Decode.succeed CoolItem
|: Decode.field "foo" Decode.int
|: Decode.field "bar" Decode.boolSo now that we have a Decoder CoolItem, all we have to do now is set up our app to decode the actual JSON string into a List CoolItem.
import Html exposing (Html, text)
import Json.Decode as Decode
view : a -> Html String
view =
text << toString
main : Html String
main =
coolJson
|> Decode.decodeString (Decode.list coolListDecoder)
|> viewDecode.decodeString : Decoder a -> String -> Result String a
Decode.list : Decoder a -> Decoder (List a)
But, go look at the demo JsonDecodeApplicative.elm.
Thinking About Elm Applicatives
So how do we find Applicatives in a language like Elm without type classes? Look for type signatures, common names, or think about what the singleton and andMap would be.
In Elm youâll see the term singleton or succeed (like Decoder and Task) for pure. âŠAnd most of the time youâll see andMap, for <*>.
So letâs see some in action which uses Decode.map and nested (|:) with some real JSONHTTP requests because some folks want to see a more real-world example about how to put this together with Task and Cmd well: PokĂ©mon Viewer demoPokemonViewer.elm.
Takeaway
If youâre just looking into the base Json.Decode that comes in the core, youâll see that Json.Decode.mapN stops at map8. Many people dead-end here and donât know where to look next when their objects are bigger. Other blog articles you might find online will often show you how this can be done with elm-decode-pipeline or something else, but they are showing you how instead of why. The why for why this works is applicativesâwhich is based in math and laws. With the applicative style, we have a tool that can cover all cases as well as an understand about whatâs going on under Elmâs hood. An important caveat though, is that these will fail silently when you write bad decoders. This can be solved using Results and the Run A Decoder of the documentationâor using the mapN functions when possible.
Special thanks to fresheyeball for explaining this shit to me.
