Elm Tricks from Production: declarative, bug-free user interfaces with custom types
This is part of a series of posts about Elm in production at Lunar Logic. Be sure the check the first post for more context.
There are 4 possible statuses when it comes to fetching data in a frontend application:
- before fetching
- while fetching
- successful fetch
- failed fetch
Elm solves both the problems defined above in perfect functional-programming style, adding a type. In particular, let’s say we want to create a new cool webapp. It will show a random number every time the page is refreshed, cool right?! We can use a public API to fetch the random number. Also, we need to choose a type, let’s go with
Int. Unfortunately, the random number API is really slow so we need to show a message while fetching. We could use 0 to mark the fact that we still don’t have the random number:
case number of 0 -> Html.text "Loading..." i -> Html.text ("The random number is: " ++ String.fromInt i)
But this is not a good solution: if the API returns 0 as a random number we will be showing the loading message forever! Let’s try with
case maybeNumber of Nothing -> Html.text "Loading..." Just i -> Html.text ("The random number is: " ++ String.fromInt i)
Cool, now we can distinguish between not having the number or having it. What if we got an error though? Let’s add a flag for that:
case (maybeNumber, hasFailed) of (Nothing, False) -> Html.text "Loading..." (Just i, False) -> Html.text ("The random number is: " ++ String.fromInt i) (Nothing, True) -> Html.text "Could not fetch random number" (Just i, True) -> WTH??
Problem is there’s a combination that does not make sense at all (i.e.
(Just i, True)): we fetched a number but there’s an error. Custom type to the rescue:
type RemoteData = Loading | Success Int | Failure case remoteDataNumber of Loading -> Html.text "Loading..." Success i -> Html.text ("The random number is: " ++ String.fromInt i) Failure -> Html.text "Could not fetch random number"
Now, let’s say that instead of loading the random number every time the page is refreshed, we want to start fetching as soon as a button is clicked:
type RemoteData = NotAsked | Loading | Success Int | Failure case remoteDataNumber of NotAsked -> Html.text "Click the button!" Loading -> Html.text "Loading..." Success i -> Html.text ("The random number is: " ++ String.fromInt i) Failure -> Html.text "Could not fetch random number"
The beauty of this solution is that, as soon as we declare our random number to be of
RemoteData type, the Elm compiler will enforce checking all the branches. Also, it’s really easy to see what the app is doing depending on the status of the request.
AirCasting uses the pattern shown above extensively. In particular, to avoid reinventing the wheel we used RemoteData. One example is the function that takes care of rendering either the sessions list or the selected session at the bottom of the map page:
viewSessionsOrSelectedSession selectedSession = div  [ case selectedSession of NotAsked -> viewSessions Success session -> viewSelectedSession (Just session) Loading -> viewSelectedSession Nothing Failure _ -> div  [ text "error!" ] ]
In other words, if the request for the selected session is not ongoing, show all the sessions. If the selected session is loading show its view while waiting for the data to come. If the request was successful show the selected session view for it. If the request failed show the error message.
If you are not using Elm you can still apply the pattern successfully. Unfortunately, there won’t be a nice compiler helping guaranteeing an error free application 😉.