I am trying to understand all the different ways in which types can be defined and pattern matched in Elm.
While searching for code to learn from, I found an implementation in pure Elm of a JSON decoder. The code can be found here and the article series here.
I cannot understand the style used in the field function:
type Value
= Jnull
| Jobject (Dict String Value)
type Decoder a
= Decoder (Value -> Result String a)
decodeValue : Decoder a -> Value -> Result String a
decodeValue (Decoder attemptToDecode) value =
attemptToDecode value
field : String -> Decoder a -> Decoder a
field key (Decoder parameterAttempt) =
let
decodeKey object =
Dict.get key object
|> Result.fromMaybe "couldn't find key"
|> Result.andThen parameterAttempt
attemptToDecode value =
case value of
Jobject foundIt ->
decodeKey foundIt
_ ->
Err "not an object"
in
Decoder attemptToDecode
The test written for the function looks like this:
test "decodes a field" <|
\_ ->
Jobject (Dict.singleton "nested" (Jnumber 5.0))
|> decodeValue (field "nested" int)
|> Expect.equal (Ok 5)
I don't understand the body of the let. Why is there an assignment like this and how does the code get evaluated? How is
Dict.get key object
handled and "bound" to?
decodeKey object = ...
attemptToDecode value = ...
Fundamentally I am trying to understand what happens in the let such that it returns something "useful" for Decoder attemptToDecode. Also, is there a better way of expressing what is intended?
Thank you in advance!
I think @zh5 makes a valid point in the sense that:
On the other hand, trying to understand something just out of curiosity is a good thing even if it is not always particularly useful, so I will make an attempt to help.
This is the intent of field
(I assume):
Give me "the logic" in a
Decoder
that I can use to decode a field and give me a field name. I will take "the logic" out of yourDecoder
, put some "extra logic" on top of it to find the field in a JSON object, and I will give you back this combined logic in a newDecoder
.
So, that "extra logic" is split into two parts in the code above:
attemptToDecode
captures the logic for ensuring that whatever is being decoded is a JSON object. The value of this JSON object is represented as a dictionary which is extracted and passed on to the second part. decodeKey
captures the other half of the logic. Having the content of the JSON object in the form of a dictionary, now we should find the field and try to decode it using "the logic" that was supplied in the Decoder
. This logic is deconstructed from the decoder and called parameterAttempt
in the code. Now, attemptToDecode
refers to decodeKey
which then refers to parameterAttempt
(the original logic passed for decoding the field), so we can say that attemptToDecode
captures the whole logic that is needed to decode a field from a JSON object. So at this point, all that needs to be done is to wrap this logic back into a Decoder
, which is exactly what the code says:
Decoder attemptToDecode
And, surely you are right in your own answer that the logic captured in the decoders is captured in the form of functions, and when these functions refer to each other their type signatures must match up at the end.