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.
This week on the podcast, Corey Haines and I talk about Elm. You should listen to it.
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.
Here’s a perfectly reasonable basic JavaScript data structure for the problem:
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
editing
state but still have@originalText
be 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 String
or 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
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
. 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 x
.
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 variablesold
and 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 old
and 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.
I’d love it if you listened to the rest of the show. Leave a comment, or follow us on Twitter.