Loading state design revisited in Elm

izumisy

Seiya Izumi

Posted on August 15, 2020

Loading state design revisited in Elm

If you are a frontend dev, I suppose that you have been frequently through the scene that you implement loading view in the frontend application.

Elm is a domain-specific language built for frontend applications, so it is pretty usual to design loading view. The great thing is that, Elm has Maybe that says if the value is available or not in a type level. Using Maybe in designing loading state in Elm is really naive pattern, but this is legit.

type alias Model =
    { user : Maybe User
    , bookmarks : Maybe Bookmarks
    }
Enter fullscreen mode Exit fullscreen mode

Now, we can implement init function as follows. Nothing is a variant that says no value available, so we can understant that this function returns empty Model anyway.

init : Model
init =
    { user : Nothing
    , bookmarks : Nothing
    }
Enter fullscreen mode Exit fullscreen mode

As I said this is a naive pattern, this Model design is failure in an Elm way.

First of all, Maybe does not carry any of context information. Maybe is just a type says it can be Nothing or Just, so it does not tell you why it is Maybe. This is meta, but the bigger your codebase gets, the more critical how to convey the context of it by code is.

Maybe is way better than Boolean, but using it too many times is almost equal to Boolean Identity Crisis.

Unleash Custom Type

Let the code tell us its context. This is exactly what Elm as a typed language should be, and where Custom Type comes into.

type Model
    = None
    | UserLoaded User
    | BookmarksLoaded Bookmarks
    | AllLoaded User Bookmarks
Enter fullscreen mode Exit fullscreen mode

Our Model is now way more descriptive than before. We can understand that bookmarks and an user are needed to be waited until fetched.

In addition to the descriptivity, this also has advantage that we don't have to explicitly unwrap them out to non-Maybe types anymore! If every single data waited to be fetched is all wrapped with Maybe, it bothers us that we always have to unwrap it every single time even at where we are sure that it would never be Nothing. We can now extract all data just by a single pattern matching of Model.

I have learnt this pattern from the video, "Make Data Structure" by Richard Feldman at Elm Europe 2018.

A page of Richard's slide
Make Data Structure by Richard Feldman

Pitfall: Combinatorial Explosion

However, we would easily come across the case that designing Model simply with Custom Type does not actually work out.

Imagine that some more additional data sources are needed to be fetched in initialization. Working with two is still duable by designing your Model with a simple Custom Type like the previous, but how about more of it? The bigger the number gets like three, four, five, the harder it is to cover all their patterns up.

This is a kind of combinatorial explosion. If you design waiting state on your Model which waits 3 resources at the intialization.Maybe has cardinaliy of 2 which is the number of exponent of waiting resources, so the result is 3^2=9. Now we can see that the number of variants on your model is 9. It exceeds 9 if additional error handling is needed.

The number of variants can easily be exploded. Using a simple Custom Type in desiging waiting state in your Model is totally unrealistic if it is more than two.

elm-multi-waitable

As one of the solution for the variant explosion in Model, I crafted a small package that encapsulate multiple Maybes from Model. This package is well tested on our product built with Elm.

GitHub logo IzumiSy / elm-multi-waitable

A small package like a traffic light

The great thing this package exactly improve is your Model design that needs waiting multiple resources. If your application has to wait 3 resources at the initialization, your Model will look like as follows.

type Model 
    = Loading (MultiWaitable.Wait3 Msg User Options Bookmarks)
    | Loaded User Options Bookmarks
Enter fullscreen mode Exit fullscreen mode

Very neat! Even if we need more of resources waiting, the variants never be exploded anymore.

The rest that wires everything up is as follows.

init : Model
init =
    Loading <| MultiWaitable.init3 AllFinished


type Msg
    = AllFinished User Options Bookmarks
    | UserFetched User
    | OptionsFetched Options
    | BookmarksFetched Bookmarks


update : Model -> Msg -> ( Model, Cmd msg )
update model msg =
    case (model, msg) of
        ( Loading waitable, UserFetched user ) ->
            waitable
                |> MultiWaitable.wait3Update1 user
                |> Tuple.mapFirst Loading

        ( Loading waitable, UserOptions options ) ->
            waitable
                |> MultiWaitable.wait3Update2 options
                |> Tuple.mapFirst Loading

        ( Loading waitable, BookmarksFetched bookmarks ) ->
            waitable
                |> MultiWaitable.wait3Update3 bookmarks
                |> Tuple.mapFirst Loading

        ( Loading _, AllFinished user options bookmarks ) ->
            ( Loaded user options bookmarks, Cmd.none )
Enter fullscreen mode Exit fullscreen mode

MultiWaitable module essentially provides statemachine-like functionality. Internal implementation of this package actually uses multiple Maybes to keep track of status of what has been done and what still not, but it is well encupsalated.

wait[N]Update[N] function MultiWaitable provides returns a tuple contains updated model and Cmd which publishes a completion Msg registered by init[N] function that initialize MultiWaitable at first. Once all waiting state internally kept is marked as completed, wait[N]Update[N] function emits a Cmd for the completion Msg.

What this package brings developers is exaclty a great concentration on Model designing itself. You would be able to design your Model in a descriptive way without any contextless, distractful multiple Maybes.

Cleaner model, greater maitainability

Model is exactly the place where has the bigger portion of clue to know how your application works at a glance, but it gets massive in no time when the application grows. This is no doubt.

Clean model does not let you misunderstand your application and reduce bugs in implementing new features. Designing loading state in a clean way is one of the easiest things we can tackle right now.

Always keep eyes on your Model and let it have enough context.

💖 💪 🙅 🚩
izumisy
Seiya Izumi

Posted on August 15, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related