Search code examples
elm

Understanding type destructuring in Elm


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!


Solution

  • I think @zh5 makes a valid point in the sense that:

    1. this is probably either not the right example for you to learn from at this point (for whatever you are trying to accomplish)
    2. or if it is, you should be able to understand it without asking this question.

    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 your Decoder, 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 new Decoder.

    So, that "extra logic" is split into two parts in the code above:

    1. 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.
      (If whatever is being decoded is not an object the result should be an error, obviously.)
    2. 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.
      (Obviously, if the field cannot be found in the JSON object the result should be an error.)

    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.