Higher Level Coding with Elm Union Types

Make Your Code More Expressive

billperegoy
im-becoming-functional
7 min readJun 13, 2017

--

Why Union Types?

In my last post, I talked a bit about how I made some small changes to my single page web application routing using union types. That was the start of a new coding addiction for me. Since I did that, I just keep finding places to improve code using better types. Taking the time to define types that allow your code to be more expressive have a couple of huge wins.

  • The code is easier to read. Programming at a higher level reveals your intent and makes code easier to maintain later.
  • Refining the types allows you to get rid of impossible states. This makes code clearer and avoids writing code that may never be executed.

For me, coding this way doesn’t come naturally. After years of working with less expressive data types, my natural tendency is to think in terms of conventional data structures (lists, hashes, etc.) wrapping low-level data types (string, integer, boolean, etc.). So instead of thinking at the level of the problem domain, I tend to immediately map the problem into concepts that conventional programming languages understand. This is a shame as the expressiveness of the original domain is often lost in this translation.

How I Learn

For me, my best learning occurs when I take a familiar problem I may have already solved and attempt to solve it again using a new technology. This can be difficult though as it’s easy to take the same mindset into the new code as I did into the original situation. So I often will start working on a problem and get totally stuck. At this point, I’ll often drop it and come back to it at a later time when maybe my mind is in a different place.

The example I’m showing today was one of those. I had attempted to solve it a while back using more conventional data types and the solution just didn’t feel right. However, at a recent gathering of the Boston Elm Meetup, I was really inspired by a talk from Joel Quenneville. I had seen talks on union types and avoiding impossible states in the past, but this talk connected more than the others. I think that’s largely because my functional programming skills have reached a point where this is all starting to make more sense. So I’m going to walk through a sample problem and how I used Elm union types to craft a solution I found much more satisfying.

The Problem

This is a very small subset of a problem I recently worked on at my day job. We were designing a form editor that allows users to build an email signup form that could be placed on their website. One piece of this editor allowed the user to choose which of the available fields should be placed on the form and which one’s would be required to be filled in by the end user. Here’s an example (trust me, the real professionally designed version looks a thousand times better than my amateur design).

Form Editor Example

You’ll note here that the user can select amongst a number fields using the edit panel on the left side of the page. For each field, the user can whether the field is to appear on the form. In addition, if the field is selected, the field can be made required or optional (required fields get an asterisk next to the name). Finally, note that some fields (email in this case) must appear on all forms. So, in this case the email field has no edit capability.

Let’s now walk through the reasoning I used to model this data.

The Naive Approach

Whenever I come across a problem like this, I usually think in terms of low-level data models and come up with something that works but isn’t very elegant or expressive. Here’s what I first came up with for this problem.

With this set of data types, you might represent a list of fields like this.

You can probably already see the first problem with this modeling technique. It’s just not very expressive at all. You can’t look at this list and understand it without looking back at the type definitions. We can make this a bit better by forgoing the constructor and being more explicit.

Well. you can read this version without looking back at the original type definition, but it certainly is ugly. It’s very verbose and still takes a bit of study to understand the code’s intention.

In addition to this obvious readability issue, there’s another more serious issue. This definition of Field allows for a number of invalid states. We’ll talk more about this next.

Invalid States

Let’s take a look at validity of the possible states with the type we used above. With three booleans, we have a total of 8 state possibilities. Which of these are valid?

  • If immutable is true, the other two booleans are meaningless. In this case, the field is always selected and required. This means that of the 4 states with immutable true, only one of them is valid.
  • If selected is false, the required boolean is meaningless. If a field is not selected, it doesn’t show up on a form, so it cannot be required. This means that 1 of the 2 states in this category is invalid.
  • Finally, if immutable is false and selected is true, the value of required is relevant. So these final two states are valid.

It’s probably easier to see this in a tabular form. In this table, the x characters represent valid states.

Valid Field States

If we were going to write code to display fields using this data model, we’d end up with a really nasty, and virtually unreadable case statement.

We can do better than that by going back to our original problem domain and thinking at a higher level.

Creating Smarter Types

In the Ruby programming world, there’s a saying that goes like this: “write the code you wish you had.” In Ruby this means you can use the expressiveness of the Ruby language and meta-programming to write code that looks the way you think about it. With Elm data types, we can do something similar.

Let’s take a look at the valid states in the table above and try to build a union type that represents only those states. My first thought is to replace those three cumbersome booleans with a single new data type. So we end up with a new field type that looks like this.

Now let’s take a pass at creating the union type that represents the possibilities. Here’s a first pass.

This is close to covering our needs but not quite. The first two cases are pretty obvious. There is only one possibility for Immutable or Unselected states. However when a field is Selected, we have two possibilities. It could be required or optional. We need a slightly more sophisticated data model to allow for this possibility. We can used parameterized union types to represent this.

It’s really that simple. With these type definitions, we can now represent the list of fields we worked with above like this.

To me, this is really amazing. I can quickly look at this and completely understand the data being expressed. I’ve really written the code I wished I had. Plus, pattern matching against this goes from a hard-to-read 8 way case statement to something much simpler.

This is so much more readable and expressive. I can read this code and immediately see everything that is going on. On top of that, all of the conditions in the case statement are legal. There is no need for fancy pttern matching to ensure the invalid states are mapped properly.

Conclusion

For most of us who are new to functional programming and Elm, expressing our data types in this more expressive way doesn’t come naturally. You’ll likely find yourself thinking in terms of lower level data types. This is quite natural, but with practice you’ll get better and identifying situations where union types can improve your code.

For me, I look for code smells where I am handling states that I think should never happen in real life and those where my data types feel complex and not expressive. When I come across these situations, I try to step back to the original problem statement and see if I can extract a new data model that expresses the high level model in a more expressive way.

This all takes some patience and practice, but I’m finding it well worth the extra effort. I’m much happier writing code this way and even happier when I go back and read code I wrote in this more expressive style.

--

--

billperegoy
im-becoming-functional

Polyglot programmer exploring the possibilities of functional programming.