Note
|
Originally for Elm v0.17. Updated 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 since 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 start
Quick 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
since Maybe
is a functor.
Maybe.map ((+) 2) (Just 1) == Just 3
--=> True
The 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
,singleton
apply
,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 a
Knowing that, let’s look at some examples of Elm singletons:
Just : a -> Maybe a
Ok : a -> Result x a
singleton : comparable -> Set comparable
flip (::) [] : a -> List a
Apply
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 b
So 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 -> Maybe (a -> b) -> Maybe b
That 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 1)
And let’s apply values to completion
baz : Maybe number
baz =
bar |> Maybe.andMap (Just 2)
isJust3 : Bool
isJust3 =
baz == Just 3
--=> True
And the same thing, creating an infix for andMap/apply:
import Maybe.Extra as Maybe
singleton : a -> Maybe a
singleton =
Just
-- NOTE: Elm no longer supports infixes :(
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) == Nothing
--=> True
Look 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
--=> True
The ` ` (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 a
Looks pretty pure and singleton-y to me…
Relevant only pre v0.18
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 b
Well 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
So https://package.elm-lang.org/packages/elm-lang/core/5.0.0/Json-Decode#field [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.int
Now that we’re set up, let’s create a CoolItem
decoder using applicative-style
A reminder about Elm types, 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.Decode.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.andMap "foo" Decode.int
-- And the full decoder, which completes the CoolItem constructor
coolItemDecoder : Decoder CoolItem
coolItemDecoder =
Decode.succeed CoolItem
|> Decode.andMap "foo" Decode.int
|> Decode.andMap "bar" Decode.bool
So now that we have a Decoder 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)
|> view
Decode.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
would be.
In Elm you’ll see the term singleton
.
So let’s see some in action which uses Decode.map
and
nested (|:)
well: Pokémon Viewer demo PokemonViewer.elm
.
Takeaway
If you’re just looking into the base Json.Decode
s and the Run A Decoder of the documentation—or using the mapN
functions when possible.
Special thanks to Isaac Shapira for explaining this shit to me.