Elm Applicatives & JSON Decoders

Mapping & Applying Our Way to Deserialization Victory

Abstract

One of the first things you will notice is Json.Decode.map & Json.Decode.map8, and the numbers between. Soon you will ask: how do decode structures larger than Json.Decode.map8—where is map9, map10, &, 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 & 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.

Note

Originally for Elm v0.17. Updated for Elm v0.18.

What we’re building towards: Pokémon Viewer working demo.

If you want to follow along & run the code for yourself

This install Elm, its dependencies, & start up Elm Reactor for you to point your browser at http://localhost:8000.

$ git clone https://codeberg.org/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 & Just 2?

Applicatives have 2 terms
  • pure, singleton
  • apply, ap, <*>

Pure

pure is a function from some 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 & PureScript (which beautifully can allow Unicode by default).

Haskell
pure :: Applicative f => a -> f a
PureScript
pure   a α. Applicative a  α  a α

Knowing that, let’s look at some examples of Elm singletons:

Maybe
Just : a -> Maybe a
Result
Ok : a -> Result x a
Set
singleton : comparable -> Set comparable
List
flip (::) [] : a -> List a

Apply

Next we peek at the ability to lift values in an applicative with apply (Haskell & PureScript):

Haskell
liftA :: Applicative f => (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
PureScript (accurately describes some things can be apply’d without having a pure)
apply   a α β. Apply a  a (α  β)  a α  a β

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 Json.Decode.succeed:

succeed : a -> Decoder a

Looks pretty pure & singleton-y to me…

Relevant only pre v0.18

And in Json.Decode.Extra we have Json.Decode.Extra.andMap & its flipped infix Json.Decode.Extra.(|:). Well that’s obviously the apply function…

Note

Later version of Elm removed the freedom to create useful infix operator & andMaps were flipped

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 & 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 & nested (|:) well: Pokémon Viewer demo PokemonViewer.elm.

Takeaway

If you’re just looking into the base Json.Decodes & the https://package.elm-lang.org/packages/elm-lang/core/5.0.0/Json-Decode#run-decoders[Run A Decoder] of the documentation—or using the mapN functions when possible.


Special thanks to Isaac Shapira for explaining this shit to me.