Loading state design revisited in Elm
Seiya Izumi
Posted on August 15, 2020
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
}
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
}
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
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.
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 Maybe
s from Model. This package is well tested on our product built with Elm.
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
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 )
MultiWaitable module essentially provides statemachine-like functionality. Internal implementation of this package actually uses multiple Maybe
s 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 Maybe
s.
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.
Posted on August 15, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.