Union Types in Elm
This is part of a new series of blog posts expanding on or relating to each episode of the Tech Done Right podcast. There are a couple of links through this post going back to specific parts of the podcast. Leave a comment, or follow us on Twitter.
Podcasts are perhaps not the world’s best medium for talking about code, so I wanted to dive a little deeper into a couple of things that Corey and I discussed.
Time And Space
Corey mentions early on in the podcast that you can’t just get the current time in Elm because it interacts in the real world in a non-replicable way. This is true, but it does not mean that you can’t work with time at all, it just means that you need to use the Elm constructs for dealing with external data — we talk about the pattern of “send a message to the system the system we want this data then respond to a different message with the data once it has been gathered”. We just don’t normally think of the system time as external data. Here’s one example of how to manage dates in Elm.
Types and Union Types
We also talked about types and union types. I think the example we talked about might be a little easier to follow with some code.
Corey gave the example of working with an editable component. This is based on a real example in the Hearken app, but I don’t have access to real Hearken code, so I’m just going based on Corey’s comments in the podcast, and based on the Editable library for Elm.
We have some text that is potentially editable by the user. When the user is editing text, we want to display an input field, and we also need to hold on to the original text in case the user cancels. But when the user is not editing the text, we don’t need to hold on to any kind of other value.
I’m omitting some details here, like exactly what we do with the data once its saved.
This is reasonable, but still has some problems:
- We’re continually carrying around an instance variable called
@originalText, even though we only need it when we are editing.
- More importantly, nothing in this code really enforces the consistency of the data. We could easily end up in an
editingstate but still have
@originalTextbe empty. The long term problem here is that we can’t trust the state value which might lead to us misusing the data or lead to subtle bugs.
These problems are all solvable. We could add a bunch of code to validate the that the instance is in a consistent state, we could write a bunch of tests, all that. But the responsibility is on us to maintain consistency, the language doesn’t do much for us.
Elm gives us a reasonably concise way to manage the same data. It might look something like this:
What this code does is define what Elm calls a union type.
Generically, a type in a computer language is a a set of possible values. Types are often used as a way of telling the underlying compiler or interpreter something about the data that will be used in a variable so that the compiler can do something with that information. Common types include things like
Number. Gary Berhardt did a very detailed overview of what a type is.
Some languages allow for types that refer to other types. For example, Java lets you declare a List<String>, which means that the data is a list of string objects.
Elm allows you to have that kind of compound type. Elm also allows you to define a type as being the union of multiple other types, meaning that the set of values in the type is made up of multiple subsets of values.
Back to those three lines of Elm, we are defining a type called
Editable is a union type with two possible kinds of value.
The next two lines define the two ways to create editable data. We can create data with the constructor
NotBeingEdited String, which takes a single value, or with the constructor
BeingEdited String String, which takes two string values. So we could say
x = NotBeingEdited "value" or
y = BeingEdited "oldValue" "newValue. Critically, as far as Elm is concerned, any time we expect a value of type
Editable that value can come from either the
NotBeingEdited constructor or the
BeingEdited constructor. The constructor helps us define the state of the data.
Using the union type solves both the extra data problem and the inconsistent state problem. Since the amout of data depends on the state, we only have the extra string when we are actually in the middle of editing. And since the state is part of the type system, the compiler enforces that restriction on the amount of data. It’s impossible to be in a state where we are not editing a string but have the extra value. The impossible state of not editing but there’s a buffer value is literally blocked by the compiler from ever existing. In Elm, the goal is to define the type system so that impossible states are impossible to compile.
How would you use this type? That’s where it gets a little verbose. Here’s a function that lets you ensure that your
Editable value is in the state of being edited. You’d call this when you start editing a value.
Let’s walk that code: The first line says that
edit is a function that expects an
Editable argument and returns an
Editable value. The second line is the actual header of the function specifying that the argument is
In the body of the function we use a
case statement to switch based on which kind of
Editable we’ve got. Elm case statements use pattern matching — essentially we reverse the constructors used to create the
Editable so we can get at the internal values. So if the
Editable is already being edited, the data was created with the
BeingEdited constructor and the first part of the case statement matches. That first branch of the case statement assigns the internal values of the
BeingEdited data to the variables
new but we don’t really need them, the data is already being edited, so we can just return the
BeingEdited value. (Technically the compiler will complain here that I’ve declared
new and not used them). If the value is
NotBeingEdited then the second branch matches, the internal data is assigned to the
value variable, and the code created a brand-new
BeingEdited item using the internal value of the
NotBeingEdited item as both the current data being edited and the original starting data.
It may seem weird to have that first clause — why do we need to tell Elm that asking to edit a value that is already being edited has no effect? The way the Elm compiler works, if we set up a
case statement for a variable, the compiler will insist that we have a clause for all the possible kinds of states of that variable — if we have a clause for
BeingEdited, we must have a parallel
NotBeingEdited clause or the code will not compile.
We can use similar functions to end the editing process: turning a
BeingEdited value in to a
NotBeingEdited value using either the new or old string depending on whether we are saving or canceling.
Similarly, you can pattern match a case statement to correctly handle
Editable values in other contexts. Here’s one that displays an
Editable as HTML, if it’s being edited, it displays the new value in a texteara, if not, it displays the value in a div tag.
So that’s what Corey and I kind of ran past quickly. Elm lets you use types to handle business logic and make it impossible to write code that puts your data in an invalid state.