The power of a Static Typed language can seem magical at first. But the goal here is to take a tiny peak behind that curtain.
Elm's implementation of JSON parsing is type safe and how it achieves that can seem like a mystery. Even though I got the code to work, it took me a while to fully understand how it works.
I'm writing it down here for 2 reasons. To help others gain a greater understanding of Types and so I don't forget what I learned.
Go through this document slowly. There's a lot of new concepts for beginners and you may want to attack this document in multiple sittings. While the code is simple, the concepts are not. I believe that it's worth spending time to get this straight in your head. So please, do yourself a favor and take your time.
So here's the code in its entirety. Don't try to understand it in one big gulp. Below this, is a step by step breakdown.
module JsonTest exposing (..)
import Json.Decode as Json exposing ((:=), maybe, string, int, float)
type alias Address =
{ number : Int
, street : String
, city : String
, state : String
, zip : String
}
type alias User =
{ name : String
, age : Int
, amount : Float
, twelve : Int
, address : Address
}
json : String
json =
"""
{
"name": "Joe",
"age": 20,
"address": {
"number": 1234,
"street" : "Main Street",
"city": "Jamestown",
"state": "CA",
"zip": "99999"
}
}
"""
(//) : Maybe a -> a -> a
(//) =
flip Maybe.withDefault
(///) : Json.Decoder a -> a -> Json.Decoder a
(///) decoder default =
(maybe decoder) `Json.andThen` (\maybe -> Json.succeed (maybe // default))
(<||) : Json.Decoder (a -> b) -> Json.Decoder a -> Json.Decoder b
(<||) =
Json.map2 (<|)
addressDecoder : Json.Decoder Address
addressDecoder =
Json.succeed Address
<|| ("number" := int)
<|| ("street" := string)
<|| ("city" := string)
<|| ("state" := string)
<|| ("zip" := string)
userDecoder : Json.Decoder User
userDecoder =
Json.succeed User
<|| ("name" := string)
<|| ("age" := int)
<|| (("amount" := float) /// 100)
<|| Json.succeed 12
<|| ("address" := addressDecoder)
user : Result String User
user =
Debug.log "user" <| Json.decodeString userDecoder json
module JsonTest exposing (..)
import Json.Decode as Json exposing ((:=), maybe, string, int, float)
This code defines the module and imports the JSON Decoding module from the core Elm libraries. It renames Json.Decode
to Json
because it's less verbose. It also exposes some of the functions we'll be using.
type alias Address =
{ number : Int
, street : String
, city : String
, state : String
, zip : String
}
type alias User =
{ name : String
, age : Int
, amount : Float
, twelve : Int
, address : Address
}
These are some example type aliases, viz. a User
and their Address
. So note that this is a hierarchical structure. So we'll expect our JSON to have a key
with a value
that is another object
.
json : String
json =
"""
{
"name": "Joe",
"age": 20,
"address": {
"number": 1234,
"street" : "Main Street",
"city": "Jamestown",
"state": "CA",
"zip": "99999"
}
}
"""
And sure enough our expectations have been met. Notice that address
is an object
.
One thing to notice here is the triple quotes """
. In Elm, this allows us to have multi-line strings and it allows us to embed double quotes without having to escape, i.e. there is no need for "\""
.
(//) : Maybe a -> a -> a
(//) =
flip Maybe.withDefault
This is just a convenience operator because I don't like the verbosity of Maybe.withDefault
. This operator is normally for Integer division where the remainder is discarded. But I almost never do that, so as far as I'm concerned this operator is up for grabs (at least in this module where there's no math).
This is also familiar to Perl programmers for define-or
operator. And it's just a slanted version of idiomatic Javascript for default values, e.g.
const f = (a, b) => {
a = a || 0;
b = b || 0;
return a + b;
}
Also notice the flip
function. If we look at this function's implementation we'll see:
flip : (a -> b -> c) -> (b -> a -> c)
flip f a b=
f b a
The flip
function does exactly what it's name implies, it flips the first 2 parameters of any function.
This was employed on Maybe.withDefault
because it had the parameters in the wrong order for this operator. The default needs to be the second parameter for this to work as hoped, which is:
notMaybe = maybe // default
In the same vein, we have a version for decoders:
(///) : Json.Decoder a -> a -> Json.Decoder a
(///) decoder default =
(maybe decoder) `Json.andThen` (\maybe -> Json.succeed (maybe // default))
This allows us to concisely write a decoder with defaults. First the decoder of some type a
is "maybe-fied" with (maybe decoder)
. So now we can write the decoder as if it's not a maybe
. Then we just provide a default of same type a
and we get back a decoder that takes the value returned from the maybe
decoder and wraps it in a Json.succeed
with a default.
We can see how it's used in the userDecoder
:
userDecoder : Json.Decoder User
userDecoder =
Json.succeed User
<|| ("name" := string)
<|| ("age" := int)
<|| (("amount" := float) /// 100)
<|| Json.succeed 12
<|| ("address" := addressDecoder)
This is nice and concise.
Next we have:
(<||) : Json.Decoder (a -> b) -> Json.Decoder a -> Json.Decoder b
(<||) =
Json.map2 (<|)
Now this function had me baffled when I first saw an implementation in a blog that credited Evan Czaplicki, the creator of Elm. My version differs from the blog in that it uses an infixed operator because I think it makes the code look nicer as we will see later. I also made this one point-free.
So let's breakdown this function slowly to see how it works.
Let's look first at the Type Signature. To understand how it goes from Json.map2
's signature to this one, we should take the implementation apart one step at a time.
First, let's recall Json.map2
's signature:
map2 : (a -> b -> c) -> Json.Decoder a -> Json.Decoder b -> Json.Decoder c
Next let's look at:
(<||) : Json.Decoder (a -> b) -> Json.Decoder a -> Json.Decoder b
(<||) =
Json.map2 (<|)
This is where I got confused. I stared at this signature for longer than I'd like to admit before I realized what was happening.
How does simply adding (<|)
as the first parameter of Json.map2
change the signature so drastically?
Here's the key, a
, b
and c
in map2
have nothing to do with a
and b
in (<||)
. Once I realized that my thinking was clouded by the use of the same names, I eventually saw how (<|)
transformed the type signature.
Thinking positionally, and substituting (<|)
's signature, (a -> b -> c)
, for map2
's first parameter, we get:
map2 : ( a -> b -> c) -> Json.Decoder a -> Json.Decoder b -> Json.Decoder c
map2' : ((a -> b) -> a -> b) -> Json.Decoder (a -> b) -> Json.Decoder a -> Json.Decoder b
Notice that the everywhere a
is in map2
, (a -> b)
is substituted in map2'
.
And where b
is in map2
, a
is substituted in map2'
. And c
in map2
is b
in map2'
.
Once map2'
gets it's first parameter, viz. (<|)
, it's signature is the same as (<||)
's:
(<||) : Json.Decoder (a -> b) -> Json.Decoder a -> Json.Decoder b
(<||) =
Json.map2 (<|)
So now that we understand the signature, what does this function do?
Well, to understand that, we have to look ahead in the code to see how it's used. Let's look at the Decoders.
addressDecoder : Json.Decoder Address
addressDecoder =
Json.succeed Address
<|| ("number" := int)
<|| ("street" := string)
<|| ("city" := string)
<|| ("state" := string)
<|| ("zip" := string)
userDecoder : Json.Decoder User
userDecoder =
Json.succeed User
<|| ("name" := string)
<|| ("age" := int)
<|| (("amount" := float) /// 100)
<|| Json.succeed 12
<|| ("address" := addressDecoder)
First notice that the decoders both start with Json.succeed <constructor>
, where <constructor>
is Address
in addressDecoder
and User
in userDecoder
.
Lets look at these parts:
type alias Address =
{ number : Int
, street : String
, city : String
, state : String
, zip : String
}
succeed : a -> Json.Decoder a
z : Json.Decoder (Int -> String -> String -> String -> String -> Address)
z =
Json.succeed Address
Notice that the constructor Address
's signature, Int -> String -> String -> String -> String
is in the order of the items in the record.
Also, notice that succeed
's signature has an a
as the first parameter. This parameter was replaced in z
with Address
's signature since the Address
constructor was passed as it's one and only parameter.
A quick reminder that signatures are parenthesized on the right:
z : Json.Decoder (Int -> (String -> (String -> (String -> (String -> Address)))))
z =
Json.succeed Address
That fact will help us moving forward.
Now, let's look at each step of the addressDecoder
. I'm going to cheat and show the generic signature for (<||)
for reference and then the substitution second.
This code is wide so you may need to scroll.
(<||) : Json.Decoder (a -> b ) -> Json.Decoder a -> Json.Decoder b
(<||) : Json.Decoder (Int -> (String -> (String -> (String -> (String -> Address))))) -> Json.Decoder Int -> Json.Decoder (String -> (String -> (String -> (String -> Address))))
step1 : Json.Decoder (String -> (String -> (String -> (String -> Address))))
step1 =
Json.succeed Address
<|| ("number" := int)
So step1
effectively "peeled off" the first type, Int
. Now step2:
(<||) : Json.Decoder (a -> b ) -> Json.Decoder a -> Json.Decoder b
(<||) : Json.Decoder (String -> (String -> (String -> (String -> Address)))) -> Json.Decoder String -> Json.Decoder (String -> (String -> (String -> Address)))
step2 : Json.Decoder (String -> (String -> (String -> Address)))
step2 =
Json.succeed Address
<|| ("number" := int)
<|| ("age" := int)
And each step will continue:
(<||) : Json.Decoder (a -> b ) -> Json.Decoder a -> Json.Decoder b
(<||) : Json.Decoder (String -> (String -> (String -> Address))) -> Json.Decoder String -> Json.Decoder (String -> (String -> Address))
step3 : Json.Decoder (String -> (String -> Address))
step3 =
Json.succeed Address
<|| ("number" := int)
<|| ("age" := int)
<|| ("city" := string)
(<||) : Json.Decoder (a -> b ) -> Json.Decoder a -> Json.Decoder b
(<||) : Json.Decoder (String -> (String -> Address)) -> Json.Decoder String -> Json.Decoder (String -> Address)
step4 : Json.Decoder (String -> Address)
step4 =
Json.succeed Address
<|| ("number" := int)
<|| ("age" := int)
<|| ("city" := string)
<|| ("state" := string)
(<||) : Json.Decoder (a -> b ) -> Json.Decoder a -> Json.Decoder b
(<||) : Json.Decoder (String -> Address) -> Json.Decoder String -> Json.Decoder Address
step5 : Json.Decoder Address
step5 =
Json.succeed Address
<|| ("number" := int)
<|| ("age" := int)
<|| ("city" := string)
<|| ("state" := string)
<|| ("zip" := string)
Notice that step5
is equivalent to addressDecoder
. Each time we add another decoder for the next record item, we reduce the signature until we finally get a Json.Decoder Address
.
The best part about the way this works is that it's Type Safe!!!
And if we look at the decoders again:
addressDecoder : Json.Decoder Address
addressDecoder =
Json.succeed Address
<|| ("number" := int)
<|| ("street" := string)
<|| ("city" := string)
<|| ("state" := string)
<|| ("zip" := string)
userDecoder : Json.Decoder User
userDecoder =
Json.succeed User
<|| ("name" := string)
<|| ("age" := int)
<|| (("amount" := float) /// 100)
<|| Json.succeed 12
<|| ("address" := addressDecoder)
We can see that they nest beautifully viz. the last decoder in userDecoder
is the addressDecoder
.
And finally the bit of test code:
user : Result String User
user =
Debug.log "user" <| Json.decodeString userDecoder json
This displays the following:
$ elm repl
---- elm-repl 0.17.1 -----------------------------------------------------------
:help for help, :exit to exit, more at <https://github.com/elm-lang/elm-repl>
--------------------------------------------------------------------------------
> import JsonTest
user: Ok { name = "Joe", age = 20, amount = 100, twelve = 12, address = { number = 1234, street = "Main Street", city = "Jamestown", state = "CA", zip = "99999" } }
:
>
I hope this article helps illuminate many things that maybe were a bit fuzzy or you didn't know existed. I feel I really learned a lot by analyzing exactly what was going on here. I hope you feel the same.