A simple custom type instead of Maybe. Am I doing this right?

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.

3 Likes