Maybe vs Custom Type
Something that can help with your example is asking for the first Entry in Entries. Given your example I’d have something like
case entries of
NoEntries -> -- do something with no entries
Entries [] -> -- do something with no entries, again
Entries (firstEntry :: _) -> -- do something with first entry
as you can see we have 2 branches that represent the same thing, which isn’t ideal. A possibly better type would be something like
type Entries
= NoEntries
| Entries Entry (List Entry)
This is admittedly very similar to only using List Entry but possibly a little more clear. Others before me gave better examples of when/why to use custom types, but this will be sufficient for the below JSON portion.
Encoding vs Decoding
Using your example of Entry and how I’d go about encoding/decoding. For a record you’ll likely want to encode to a JSON object, which is essentially a list of key/value pairs (aka List ( String, Json.Encode.Value )). It might look something like
encodeEntries : List Entry -> Json.Encode.Value
encodeEntries entries =
Json.Encode.list encodeEntry entries
decodeEntries : Json.Decode.Decoder (List Entry)
decodeEntries =
Json.Decode.list decodeEntry
encodeEntry : Entry -> Json.Encode.Value
encodeEntry entry =
Json.Encode.object
[ ( "id", encodeId entry.id )
, ( "text", Json.Encode.string entry.text )
]
decodeEntry : Json.Decode.Decoder Entry
decodeEntry =
Json.Decode.map2 (\id text -> { id = id, text = text })
(Json.Decode.field "id" decodeId)
(Json.Decode.field "text" Json.Decode.string)
But what about a custom type?? I hear you ask!
Well that is where we have to get a little creative. This is one approach to how I might encode the above Entries type that I defined
encodeEntries : Entries -> Json.Encode.Value
encodeEntries entries =
case entries of
NoEntries ->
Json.Encode.null
Entries firstEntry restEntries ->
Json.Encode.object
[ ( "first", encodeEntry firstEntry )
, ( "rest", Json.Encode.list encodeEntry restEntries )
]
decodeEntries : Json.Decode.Decoder Entries
decodeEntries =
-- this handles being encoded as `null`
Json.Decode.nullable
-- this handles being encoded as an object
(Json.Decode.map2 (\firstEntry restEntries -> Entries firstEntry restEntries)
(Json.Decode.field "first" deocdeEntry)
(Json.Decode.field "rest" (Json.Decode.list decodeEntry))
)
-- ☝️will give us either `Nothing` or `Just (Entries first rest)`
-- so 👇 converts the `Nothing` to `NoEntries` and removes the `Just`
|> Json.Deocde.map (Maybe.withDefault NoEntries)
Another approach to encoding/decoding custom types, say like
type Shape
= Circle { radius : Float }
| Rectangle { width : Float, height : Height }
is to use a JSON object with a kind and then the rest of the data, kind of like
encodeShape : Shape -> Json.Encode.Value
encodeShape shape =
case shape of
Circle details ->
Json.Encode.object
(( "kind", Json.Encode.string "circle" ) :: encodeCircle details)
Square details ->
Json.Encode.object
(( "kind", Json.Encode.string "rectangle" ) :: encodeRectangle details)
which would result in JSON like
{
"kind": "circle",
"radius": 5.5
}
This brings us to codecs and why you might want them. I find them to be useful when I need to save and load a lot of Elm types/data and never read that data outside of Elm. I let the codec decode how to handle the encoding/decoding and it typically does so in a compact way which can be very nice. If you need to send this data to an outside source which also needs to read it or you’re reading in data from outside sources and don’t need to save it, then you likely won’t want to use a codec.