I’m still a beginner in Elm but also in typed functional programming in general, so sorry in advance if this question sounds weird. I came across the Json.Decode.Decoder type while reading the Todo MVC’s source code, and I went to look at the type definition:
{-| A value that knows how to decode JSON values.
-}
type Decoder a = Decoder
… which doesn’t make much sense to me. Is the Decoder on the right a different thing from the Decoder on the left? I then looked the definition of Json.Decode.succeed and Json.Decode.fail and got more confused because they seem (?) to return the same type: Decoder a.
As a parallel, the Result type makes sense to me, it has either a success or error value. But the decoder seems to be the same for both succeed and fail? Maybe there are some concepts I still need to learn in order to understand this.
If anyone could point me in the right direction that would be great, thanks!
The Decoder functionality is implemented in what is called Kernel code - the javascript underlying elm.
The compiler cannot typecheck javascript code, but we still need types on the elm side.
For instance, a few lines below that definition, we see
The compiler cannot verify that we give the correct type to string: it will accept any type (and we could lie about the type). So, we create an empty type (there is one constructor, but it is never used and not exposed) to give types to the decoder primitives (like string and int but also map and andThen). These types then make sure that our elm code defines valid decoders.
But conceptually, the decoder could be defined as
type Decoder a = Decoder (String -> Result DecoderError a)
Yes, the left one is a type, the right one is a value. so this would be valid code
type A = A
a : A -- value `a` has type `A`
a = A -- value `a` is equal to `A`
That is really because the error is always a string. I hope this makes more sense given the elm-definition of Decoder, which which you can define
succeed : a -> Decoder a
succeed value = Decoder (\_ -> Ok value)
fail : String -> Decoder a
fail error = Decoder (\_ -> Err error)
If I may ask another question about types and values, when you have a lower case in the type it means it can hold anything, right? For example (from the Elm guides) type List a = Empty | Node a List. But in the decoder example it only appears on the left: type Decoder a = Decoder.
As an example, I can declare my own type with type Val a = Val, but it throws an error when I try to call with any value:
`Val` is not a function, but you are giving it an argument.
It seems the a in the type doesn’t change anything, so I could as well declare it as type Val = Val. Am I missing something or is this something specific for Kernel code?
Declaring a type like type Val a = Val is a fancy trick called a phantom type. We’re parameterizing the type even though we’re not using the parameter in the value. This allows us to do fancy things like:
stringVal : Val String
stringVal =
Val
boolVal : Val Bool
boolVal =
Val
boolValsOnly : Val Bool -> String
boolValsOnly val =
"Val Bool only!"
The compiler will allow boolValsOnly boolVal but will rejectboolValsOnly stringVal. Even though under the hood, both boolVal and stringVal have the exact same implementation, the compiler sees these as having different types (because of the explicit signature).
Cool party trick but what’s the use? You’ll see it in the core library when a type needs to be parameterized but the actual implementation is done with kernel code. It’s also used in some libraries like elm-tagged to give some extra type safety allow the compiler to know that your number is associated with a particular unit.
BTW, if you’re interested in getting a conceptual understanding of what the Json.Decode.Decoder and Json.Decode.Value types mean under the hood, this series of articles walks through implementing your own in pure Elm.
While fun and educational, it’s worth noting that you absolutely can master the concept of decoders without knowing how they work under the hood. If you’re newer to Elm/decoders, I’d recommend getting a feel for the various ways you can decode json and combine simple decoders into more complex ones before diving too deeply into how they’re implemented