Elm Models as Types

An idea for using Union Types over Type Aliases

I’m building a small single page application for managing Lightning Talks. In the past, the app’s Model looked something like this:

type alias LightningTalkStore =

{ asyncError : Maybe String

, editLightningTalk : LightningTalk.Model

, formErrors :

{ description : String

, speakers : String

, timeslotId : String

, topic : String

}

, isCreatingOrUpdating : Bool

, isDeleting : Bool

, isFetching : Bool

, lightningTalks : List LightningTalk.Model

, talkIdToDelete : Maybe String

}

Actually, that’s not the entire model. That was one slice of it. The entire model actually looked like this.

type alias Model =

{ appStore : AppStore

, lightningTalkStore : LightningTalkStore

, roundStore : RoundStore

, routingStore : RoutingStore

, timeslotStore : TimeslotStore

}

The above model representation isn’t all that great. The model is capable of literally thousands of different permutations, but only a fraction of those are valid states of the application. More importantly, keeping the model in a valid state relies on developer discipline which is not something we should rely on.

Rely on your tools. Tools don’t lapse in discipline, but people do.

I set out to fix this problem by changing the model to rely more on the compiler to ensure that the application is in a valid state. There were a couple of presentations that inspired me toward a solution involving Union Types rather than a gigantic Type Alias that can represent a vast number of invalid states.

Inspiration from the Elm Community

A few months ago I went to Elm Conf and saw Richard Feldman’s talk on making impossible states impossible. That talk described how to represent the internal model of a package in such a way that invalid states were literally impossible because an invalid state would cause a compiler error.

A few weeks ago I watched Kris Jenkins’ talk on doing something very similar, but this time it was about making impossible asynchronous states impossible. (I highly recommend watching both of these talks.)

At that point I wondered if I could expand these ideas further to the entire model of a production application.

Union Types over Type Aliases

I went down a road to turn my model from a Type Alias into a Union Type. To be clear, I’m still iterating on this idea so it may not be what you should do. Having said that, I’ve already eliminated a handful of bugs by using a Union Type and I feel this is worth sharing now so that I can get feedback from the community.

The same model you saw above now looks like this:

type Model

= NoData Date

| Loading Page Data

| Show Page Data Modifier

At a high level this application can be in one of three states:

NoData - The app has just loaded so we have a date but no async data

- The app has just loaded so we have a date but no async data Loading - The app is querying for data and maybe it has partially retrieved data

- The app is querying for data and maybe it has partially retrieved data Show - We have all of the async data we need and we’re showing a page in the application

To further expand on those..

type Page

= UpcomingTalks

| PreviousTalks

| CreateEditTalkForm type Modifier

= WithNoSelection

| WithTimeslotSelected Timeslot

| WithDeletePrompt DeleteFormModel

| WithDeletingPrompt DeleteFormModel (Maybe Http.Error)

| WithDeletePromptError DeleteFormModel Http.Error

| WithTalk TalkFormModel FormType

| WithTalkSubmitting TalkFormModel FormType (Maybe FormError)

| WithTalkFormError TalkFormModel FormType FormError

Here we reveal that this Single Page App has only three pages. One for viewing upcoming lightning talks, one for viewing previous talks, and another for creating or editing talks.

Additionally, we see that Modifier tells us more information about the Page. Do we have something selected? Are we submitting a form? Does the form have an error?

You may be wondering why I structured the Show constructor the way I did. Let’s take another look at it.

Show Page Data Modifier

My intent was that it reads somewhat naturally: <verb> <adjective> <noun> <qualifier>. Here’s an example of how it’s used in a case..of in an update function.

GoToUpcomingTalks ->

Show UpcomingTalks data WithNoSelection ! [ Cmd.none ]



...



DeleteTalkError httpError ->

case model of

Show UpcomingTalks data (WithDeletingPrompt talk _) ->

Show UpcomingTalks data (WithDeletePromptError talk httpError) ! [ Cmd.none ] ...

When we receive the GoToUpcomingTalks Msg then show the upcoming talks page data without anything selected.

When there is an error deleting a talk, get the data and talk to be deleted from the current page, and show the same page but with an error message.

Concluding Thoughts

As always, it’s useful to look at the trade-offs.

As you can see in the above code example, the content of the update function is harder to read at first. Ignoring the fact that Medium prematurely wraps the line, the DSL is unfamiliar. But familiarity is not a disadvantage once you become familiar with it.

On the positive side, we need to rely less on developers being disciplined with flipping the right boolean values, turning Just somethings into Nothings, or keeping other stateful parts of a giant type alias in the correct state. Most importantly, the number of impossible states has significantly reduced in number.

I’d like to hear back from you, the reader, on ideas of how to improve this concept further or if you foresee demons I have yet to come across in going down this path. There is absolutely room for improvement on this pattern but I’ve gotten it to a point where an outside perspective would be helpful.