Creating the User Decoder (user_decoder)

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Note: As of elm version 0.18 the Json.Decode.object3 function has been removed and replaced with Json.Decode.map3. All the objectN functions have been replace with mapN functions. However, there input/output annotations have not changed. This tutorial is written assuming elm version 0.17, however updating changes to 0.18 are trivial and are provided.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — -

However, before we can apply the decodeString function we must first construct the User Decoder, consisting of the tagger and the JSON Field Decoders, fieldname := valueDecoder , the fields we want to extract from Json Object (Note: Field Decoder means this entire expression (fieldname := valueDecoder)). In order to construct the User decoder we will make use of the Json.Decode.object3 function . The User Decoder, user_decoder , will consists of the following elm elements:

— — — — — — — — — — — — — — — — — — — — — — — — — -

user_decoder =

object3 User

(“id”:=int)

(“name”:=string)

(“email”:=string)

— — — — — — — — — — — — — — — — — — — — — — — — — -

object3 + 4 args: (tagger) and (3 decoders):

object3 (tagger)

(field_decoder a)

(field_decoder b)

(field _decoder c)

{-- == object3 User (“id”:=int) (“name”:=string) (“email”:=string) --}

Let’s decompose user_decoder a bit further, by looking more closely at the tagger function, User .

Json.Decode.object3 User: (a -> b -> c -> value) --(generic annotation) {--

where, User

== (Int -> String -> String -> User) == User Int String String -- Tagger (User Constructor) --}

(here we do the type substitution, again this is not actual code, but a device to aid understanding, below is the actual code)

where the User is constructor function, typically called a tagger, can be used to construct a User Record:

User 234 “julia” “julia@me.com”

creates the record,

{id = 234, name = “julia”, email = “julia@me.com”} (“id” := Json.Decode.int) -- == (maps to Decoder a ) (“name” := Json.Decode.string) -- == (maps to Decoder b) (“email” := Json.Decode.string) -- == (maps to Decoder c )

because Json.Decode.object3 requires these arguments, first arg is a tagger (or a function which takes 3 elm data types inputs and produces a desired value which will be used to create the user defined value decoder to decode the JSON Object) and 3 decoders one each for the corresponding fields in the JSON Object.

The tagger/function along with the 3 decoders are used to generate the desired User Defined Value Decoder (the Decoder for the JSON Object), in our case the User Decoder, user_decoder :

The 3 Decoders (Decoder a, Decoder b, Decoder c)

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Note: As of elm version 0.18 the (:=) operator has been removed and replaced it with field function which is not an infix function, therefore it will only accept its two (2) arguments in the post position,

field string_arg decoder_arg

This tutorial is written assuming elm version 0.17, but to update to 0.18 the code changes are trivial and are provided.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — -

All 3 decoders make use of (:=) JSON Field Decoder Operator. This operator is a function which decodes the field with the given name in the JSON Object or fails if it does not exist. It takes two arguments, a String and Decoder (value Decoder). These three individual decoders (Field Decoders) are required to create the user defined value decoder using Json.Decode.object3 (or Json.Decode.map3 in elm v0.18) necessary to decode the JSON Object into a User Record.

Note the distinction between Field Decoders (made up of a string and a value decoder, i.e. fieldDecoder == ("id" := Json.Decode.int) , Value Decoders valueDecoder == Json.Decode.int and User Defined Decoder (our desired end product user_decoder, the result of applying the Json.Decode.objectN function)

Json Field Decoder Operator Annotation:

(:=): String -> Decoder a -> Decoder a

It can be used as an infix operator where its arguments position relative to the operator are the following:

(arg1:String) operator (arg2: Decoder)

“int” := Json.Decode.int

or

operator (arg1:String) (arg2:Decoder)

(:=) “int” Json.Decode.int

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Note: As of elm version 0.18, Json.Decode.field function constrains its arguments to this second format :

field arg1 arg2

{--



== field string decoder

== field “id” Json.Decode.int --}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Recap

Up to this point we decomposed functions into there component parts and made several substitutions to trace out all the inputs and outputs necessary to create a User Decoder and return a User Record from the JSON Object using the Decoder we created from applying the Json.Decode.object3 function. We learned that if we want to extract more than 8 items from a JSON object we can’t use objectN without some clever workarounds which were not discussed here. However, now that we have sufficient background we now can introduce the Pipeline API to see how easy it is to get around this limitation.

Pipeline API just like a cascading waterfall

The Pipeline API allows us to use the elm Forward Pipe Operator, |> , to build our User Decoder by passing a User Constructor Decoder (tagger Decoder) to a chain of Value Decoders. Again we will use decomposition and substitution to map/trace inputs and outputs for an understanding of how Pipeline API works. Let’s see how we would execute the same behavior using the Pipeline API:

user_decoder =

Json.Decode.Pipeline.decode User

|>Json.Decode.Pipeline.required “login” Json.string

|>Json.Decode.Pipeline.required “id” Json.int

|>Json.Decode.Pipeline.required “name” Json.string

|>Json.Decode.Pipeline.required “company” Json.string

|>Json.Decode.Pipeline.required “blog” Json.string

|>Json.Decode.Pipeline.required “followers” Json.int Json.decodeString user_decoder json_string

or alternatively

import Json.Decode (decodeString, int, string) import Json.Decode.Pipeline exposing (decode, required) user_decoder =

decode User

|>required “login” Json.string

|>required “id” Json.int

|>required “name” Json.string

|>required “company” Json.string

|>required “blog” Json.string

|>required “followers” Json.int decodeString user_decoder json_string decodeString user_decoder json_string

Outputs an elm User Record with 6 member values:

{

login = ”julia3"

,id = 234

,name = “julia”

,company = ”MusicBus”

,blog = ”https://musicbus.com/blog”

,followers = 1300

}

Note: Our new User Record has been updated to accept 6 values we could go well over the 8 item without limitation of Json.Decode.objectN functions using this new Decoder Pipeline Pattern.

type alias User = { login: String ,id : Int ,name : String ,company : String ,blog : String ,followers : Int }

User Constructor Function(tagger):

User “julia3” 234 “julia” “MusicBus” “https://musicbus.com/blog” 1300 {--

mapping to tagger's annotation represents:

User String Int String String String Int

--}

Creates a User Record:

{

login = ”julia3"

,id = 234

,name = “julia”

,company = ”MusicBus”

,blog = ”https://musicbus.com/blog”

,followers = 1300

}

The Pipeline API introduces two new helper functions, decode and required which allow us to conveniently use the Pipe Operator to chain decoders together to create a User Defined Value Decoder , or in our case, the User Decoder, user_decoder , needed to decode our JSON Object.

But first let’s take a slight detour, let’s build our own Pipeline functionality by just using Json.Decode API; we will not use the Pipeline API. This will give us a better understanding of how this new Decoder Pipeline Pattern code works under the covers.

In order to create our own Pipeline functionality will need to understand how these new functions, decode and required work. The best place to look is in the NoRedInk’s Pipeline API source code. Let’s start with decode :

decode: a -> Decoder a decode a = succeed a

The decode function just wraps Json.Decode.succeed function to allow it to be given a more user friendly name, appropriate to the job it is doing. It basically creates a Decoder out of anything that’s passed to it. In our case we pass our tagger, User , by doing so, we create the tagger Decoder, the User tagger Decoder. However, this is not yet our User Defined Value Decoder (User Decoder). We will need to transform the User tagger Decoder into a User Decoder by adding the Pipe Operator and the other Pipeline function required into the mix. Let’s take a closer look to see how these all three work together.

Pipe Operator

The Forward Pipe Operator , |> , is a function, found in the Basics module of the elm Core Package Library (there is also a Backward Pipe Operator, <| , not discussed here). Let’s look at it’s elm annotation and function definition. This translates to,

Take any a and pass it to a function, f (a)= b , defined to take an a and return a b, then return that b

Decomposition of (|>)

(|>) : a -> (a -> b) -> b x |> f =

f x

Substitution :

-- i.e., x=1, f= x+1, 1 |> f {--

== f 1

==( 1)+ 1

== 2

--}

Json.Decode.Pipeline.required

Finally, let’s look at required.

required Function Annotation

required : String -> Decoder a -> Decoder (a -> b) -> Decoder b

required Function Definition

required key valDecoder decoder = custom (key := valDecoder) decoder

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — -

Note: As of elm version 0.18 the function definition for required would probably be updated to replace the (:=) operator with field function in the following manner:

required key valDecoder decoder =

custom (field key valDecoder) decoder

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — -

Let’s drill down deeper by decomposing required a bit further and then use substitution to understand the required function a bit better. Clearly, we can see that required accepts three (3) arguments, key , valueDecoder and decoder .

What are these 3 arguments?

They are,

key,

key

{--

== String --} {-- (name of a field in the JSON Object we want to extract) --}

valDecoder,

valDecoder {--

== Decoder a --} {-- (a value type decoder corresponding to the value type of the field in JSON Object we want to extract, i.e. Json.Decode.int, etc…)

--}

and decoder,

decoder {---

== tagger Decoder

--}

{--

(the decoder that cascades across the pipeline and is transformed from the tagger decoder step by step to the specific Value Decoder which turns a JSON Object to an elm data type, in our case, the User Decoder, user_decoder).

--}

We will be able to understand required a bit better by creating our own Pipeline analogues for both decode & required , and chaining them via the pipe operator, |> .

Let’s begin

Substitution & Decomposition of decode & |>

I will create an analogue to decode function and rename it toDecoder using more user friendly names for the function itself and its argument, since we are actually converting our tagger into a tagger Decoder. The toDecoder is equivalent to Pipeline API’s decode .

toDecoder Annotation & Definition

toDecoder: a -> Json.Decoder a

toDecoder tagger =

Json.succeed tagger

Now we will look at the User constructor function, in this case, our tagger function

User Constructor Function’s Annotation (tagger):

User: String -> Int -> String -> String -> String -> Int -> User

Now let’s see the transformation of the tagger into a tagger Decoder by applying toDecoder function to our tagger, User.

(tagger Decoder):

toDecoder User {--

== Decoder (String->Int->String->String->String->Int->User)

--}

Now let’s create a analogue for required function. I don’t like the name required, since it really doesn’t explain what the function does, so I renamed it to pipeDecoderBldr, yes it verbose, but the name makes its purpose clear to me.

import Json.Decode pipeDecoderBldr: String

-> Decoder a

-> Decoder (a -> b)

-> Decoder b pipeDecoderBldr key valueDecoder taggerDecoder = object2 (|>) (key := valueDecoder) taggerDecoder

Note: for elm v0.18 change pipeDecoderBldr to the following:

pipeDecoderBldr key valueDecoder taggerDecoder = map2 (|>) ( field key valueDecoder) taggerDecoder

Now if we begin the pipeline by using the Forward Pipe Operator and the first value Decoder , corresponding to the first argument of the tagger Decoder, we will begin to see the transformation of the the tagger Decoder. The tagger Decoder is being reduced down by a chain of value Decoders with each step (while internally creating field Decoders at each step) it gets closer to becoming our User Defined Value Decoder (our User Decoder), as it does so, in the final step of the chain.

But in order to see this we must take another look at the required function.

If we take a closer look at required , we see the function custom in its function definition. This function is what helps make all the pipeline magic happen, it wraps the Json.Decode.object2 ( or now Json.Decode.map2 for elm v0.18) function followed by a Forward Pipe Operator function, the first argument to object2 .

custom: Decoder a -> Decoder (a -> b) -> Decoder b custom =

Json.Decode.object2 (|>)

We can see custom’s function annotation, but what’s actually going on?

We need to take a look at object2 and (|>) function annotations side by side and do some substitution to clearly understand what is going on.

object2 (map2 , elm v.0.18)

object2: (a -> b -> value)

->

->

-> : (a -> b -> value)-> Decoder -> Decoder -> Decoder value

Forward Pipeline Operator

(|>): a -> (a->b) -> b

object2 takes 3 arguments, a function and two decoders and returns a value decoder, a decoder for the last value of the function.

custom has already curried the first argument of object2 , the Forward Pipe Operator function, so in fact, via the required function we are passing the 2nd and 3rd arguments, the field Decoder and the tagger Decoder respectively, via the wrapped object2 in the custom function.

Let’s do the substitution,

object2 (|>) a ==> a b ==> (a -> b) value ==> b

Transformation,

custom {--

== object2 (|>) == object2 (a -> (a->b) -> b ) == Decoder a -> Decoder (a->b) -> Decoder b --}

The custom function builds the field Decoder from the arguments passed into it by required and takes a tagger Decoder, then reduces the field Decoder, Decoder a, from the tagger Decoder, Decoder(a->b) , and then returns the balance of tagger Decoder, Decoder b,by using the properties of the Forward Operator function which in the final step of a pipeline chain results in the desired User Defined Value Decoder, in our case, the User Decoder (user_decoder == Decoder User).

In our case we have not written a custom function, we have just inlined the object2 function in our analogue to the required function , pipeDecoderBldr.

pipeDecoderBldr key valueDecoder taggerDecoder = object2 (|>) ( key := valueDecoder) taggerDecoder

Our version of the Pipeline Decoder which creates a User Decoder, user_decoder , using only the Json.Decode API looks like this,

user_decoder = toDecoder User |> pipeDecoderBldr "login” Json.Decode.string |> pipeDecoderBldr “id” Json.Decode.int |> pipeDecoderBldr “name” Json.Decode.string |> pipeDecoderBldr “company” Json.Decode.string |> pipeDecoderBldr “blog” Json.Decode.string |> pipeDecoderBldr “followers” Json.Decode.int

We would then finally use our pipeline User Decoder to decode our JSON Object in the same manner as before: