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

A very simple custom type demo
What are the benefits over Maybe?

Sorry if this question is hard to follow, I’m struggling a bit to phrase things properly!

Unfortunately the two threads I started have closed now, but I’d like to get people’s thoughts about staying sane with Maybe and representing custom types as json in the context of a very specific snippet of code. Here’s the demo and the custom type:

type alias Id =
    Int
    
type alias Entry =
    { id : Id
    , text : String
    }

type Entries
    = NoEntries
    | Entries (List Entry)

That took me a while to get right, and I’m still not 100% sold it’s better than Maybe. On line 142 of the demo we’re having to unwrap the custom type — it’s more specific, and it might (as one user mentioned) help keep impossible states impossible, but I imagine I’m still going to have to unwrap/wrap it like line 249 of this more complex example from “Programming Elm”, which uses Maybe.map.

Maybe.map updatePhoto maybePhoto

I guess I could create my own map function, but you don’t get a helper function like Maybe.map when using a custom type. So far it seems some people seem to prefer getting specific with custom types, and others are happy to use Maybe. Is it simply a matter of preference?

I definitely understand custom types usefulness for the “big” view when caseing for NothingThereYet or SomethingThereNow content (such as a json response). But for general checking of whether a data point is there or not, Maybe seems to cover a lot of bases.

The hard part: saving/retrieving as json

I’m also quite thoroughly confused about how to properly encode and decode things. Simple types are quite easy, but when you start combining them it gets harder. It seems that Json.Decode is a lot more extensive than Json.Encode. If anyone has some resources for tutorials/videos I’d be very grateful.

I understand building up basic decoders and using say, map3 to turn a json object into an Elm record, but …

  1. How do you do it the other way round? Elm record → json object.
  2. How do you do it with an Entries type?
    • Entry [{ id = 0, text = "entry"}] to {"id" : 0, "text" : "entry"}
    • It seems (below) that Encode.object expects a tuple
    • Do you need to change your record into a List tuple first?
  3. How do you combine the individual json strings with other encode values?
    • I can see how you’d create an encoded list like "[True, False, True"]
    • But most json files are pretty big; Json.Encode doesn’t do a good job of explaining how to build up bigger objects and lists.
Encode.object
        [ ( "name", Encode.string "Tom" )
        , ( "age", Encode.int 42 )
        ]

One example of a bigger json file might be:

  • A Person and their entries
  • Multiple Persons in the file, each with their List entryObjects

How on earth would you go about that?

A small note on codecs

I’m generally all for simplicity, so it seems like codec libraries add difficulty that I might not want. How many of you are using them?

TL;DR

So TL;DR I’m finding it difficult to understand the benefits of using a type Custom over a Maybe type, and how one would build up a json file from smaller parts, especially when using a type Custom or Maybe.

For your Entries type, it is as you suggest equivalent to Maybe. For me, the choice would come down to how accurately the names Nothing and Just (along with the name of the value in question) represent the data I’m talking about.

I will add another thing to think about from your example, though, and that is the type alias Id = Int. For this, I would use a custom type type Id = Id Int. This is because the custom type will prevent you from getting different ID types mixed up (for example, if there were type OtherId = OtherId Int, then the compiler would prevent you from providing an Id as an OtherId or vice versa), whereas a type alias will not.

3 Likes

It seems that Json.Decode is a lot more extensive than Json.Encode.

I think the reason for this is that when encoding, we start with a structured type (an Elm type) and create a less structured type (json), so there aren’t so many edge cases to handle. When decoding, we start with the less structured type, and create the more structured type, so we need to handle all of the edge cases where the less structured type can express something not possible in the more structured type.

1 Like

I think in this situation it’s not really an improvement, but that’s also because your List models both “Empty” and “One or more elements” which overlaps with your NoEntries.

If you don’t mind i would just focus on one example were custom types (hopefully) improve code compared to using Maybes:

One situation where custom types shine compared to sticking Maybes in the model is when it comes to multiple connected values. Perhaps you haven’t had a situation like that yet? One boring yet often very easy to understand example would be modeling somebodies role in school - a teacher with a subject or just a general pupil:

type alias Role =
  { is_teacher: Boolean
  , is_pupil: Boolean
  , subject: Maybe String
  }

I hope this seems fine at first? But it allows for impossible state being created:

{ is_teacher = False, is_pupil = False, subject = Just "Maths" }

The Maybe isn’t the root cause of it, but it’s just a “lazy” way to model the data - there is a custom type that models the situation perfectly:

type Role
  = Pupil
  | Teacher String

Now you ALWAYS have a subject for a teacher, because you cannot create a teacher without a string. Any you never have a subject for a pupil. Both of these impossible situations have been made impossible and are validated by the compiler. You also cannot have neither or both roles, but that’s not really result of wanting to fix the Maybe situation.

Hope that makes sense!

5 Likes

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

I think you need to experiment what works for you. Start with the simplest thing first, until you find that you need a custom type to convey useful information.

For example instead of having

type Entries
    = NoEntries
    | Entries (List Entry)

you could just have (List Entry). That custom type doesn’t appear to bring anything valuable, just having an empty list will mean no entries.

I would use a custom type when I have useful information to convey

type Collection
  = CollectionNotFound
  | Colllection Id (List Entry)

E.g. here we make a distinction between not finding a collection resouce and not having anything in that collection.

2 Likes

I’m not sure if I’m fully addressing your question, but here are some thoughts that might be helpful:

For handling data that’s loading from a server, you might wanna check out the RemoteData package

It gives you NotAsked, Loading, Success, and Failure states. This could be more precise than Maybe if you’re fetching data.

Now, regarding the specific problem of handling entries, I agree with Sebastian’s suggestion. You could just use a simple List and pattern match on it:

case entries of
    [] ->
        handleZeroEntries
    nonEmptyListOfEntries ->
        handleEntries nonEmptyListOfEntries

However, there’s a potential issue here. The nonEmptyListOfEntries is still just a List Entries, so you’d need to handle the empty case in handleEntries again. To solve this, it’s worth looking at the elm-nonempty-list package

On how well Entries describes my problem

@IloSophiep and @Sebastian my thinking here was to ping the server for a json response and instead of a Maybe it’d be a potential NoEntries — but I meant that even the [] empty list might not be there. I agree that it doesn’t bring much to the table in other instances.

@Sebastian Could you give me an example json file that’d work alongside your Collection type? When would you use a collection over simpler json data?

Start with the simplest thing first, until you find that you need a custom type to convey useful information.

Sounds like good advice.

@IloSophiep I’ve watched the video where @rtfeldman goes over the school visit example, so I understand this. So, when you’re using more normalised SQL type data it could be useful to avoid error. I have a situation where I’d probably want to use tags (which are often many-to-many relationship) so perhaps that’s a good use case?

I have no idea how you’d structure routing and search capabilities for those tags though. I’m not that far ahead with Elm!

@dta That Id Int sounds like a good call, or I suppose you could just use a uuid? Or would that still not solve the problem? Would you still create different ID types with a unique id?

Json.Encode

I think I understand this now, so you simply need to create a function that extracts the data you need and converts it into a List Tuple of key/value pairs.

An example of nested json data

See this demo for:

  • A json object that …
  • Contains another object …
  • That in turn contains a List Int

Do any of you have examples of nested json data that’s been created by Json.Encode? For example, a json object that contains another object that contains a List String. I can’t picture in my head how that might look!

simpleData : Encode.Value
simpleData =
  object
    [ ("name", string "Tom")
    , ("list", (list int [1, 2, 3]))
    ]

-- nestedObject
encode 0 object
           [ ("Collection", int 1)
           , ("Nested", simpleObject)
           ]

I solved my own question :wink:

The first Entry (or [])

@wolfadex Hmm. So at the moment I’m not handling the eventuality for just one entry, and I’m duplicating work for NoEntries and [] (although see above for what I was trying to do). I hadn’t thought of it that way.

Entries Entry (List Entry)

Would that mean I’d have to unwrap it twice? One for Entries and one for Entry to get at that List Entry? Or is there a shorthand? I was a little confused about the difference (I thought at first it was a recursive type!); it seems similar to Random.uniform?

Entries (Entry 1 "one") [Entry 2 "two", Entry 3 "three"]

The more I look at it a List Entry within a Collection might make more sense. And the more I see complication arise, like hacking json to store custom types, the more I want to go back to as simple as possible! I feel unless there’s a graceful way to do this (like what Evan might have in store for SQL) it’s may be wise to store as simple json, not as types.

I like the idea of codecs, and can sort of see how one might work but I’d need a few concrete tutorials to feel comfortable turning a bunch of custom types into a structured (nested) json file, or updating one with put or patch.

I find as a program grows and things become more connected, nested, and complected, my brain sort of goes meltdown and I can’t fit the bits together in my head :joy:

How many Maybes is too many?

So this brings me to my final thought, in that I guess in an ideal world you’d want to have as much control as possible over the json. In many forms there’ll be a lot of optional field types which would result in potentially a lot of Maybes. @Sebastian says to not store these as default empty values and (I think) not Json.Encode them at all and show empty view sections where appropriate.

It seems you want one main way to show if a json response comes back empty, and then Maybe.withDefault as appropriate for the individual object values and json elements in your view.

Are there any other tips to reduce the amount of Maybes? For data points that are dynamic or missing, that we haven’t already covered here?

I would still wrap the id type, whether it’s wrapping an Int, a String, or a uuid. The benefit is to be able to separate different id types from each other, not just from things that aren’t ids.

1 Like

A photo album would be an example of this. You want to distinguish between album not found vs album found but has no photos.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.