Making Elm code more compact and more readable

Using extractions for friendlier code

When I started in Elm, whenever I needed to update something in a List, the approach I always reached for was this:

  1. Get the relevant item from the list
  2. Update the item
  3. Put the new item back in the list

In code, this would look something like this:

Perfectly valid code. And there is nothing wrong with using these let .. in .. blocks to define intermediate variables.

Maybe this pattern was second nature, because of my javascript background. Where it is really cheap and easy to define some temporary var somewhere, use it, and forget about it.

But still, quite a bit of code to update an item in our list.

Check code efficiency

Let’s look at our code in more detail, step by step:

In step 1: To get the item to update from our list, the smaller steps are

  • loop through all items in the list
  • if any item has the id we are looking for, include it in the intermediate result list.
  • then take the first result, which could be Nothing

In step 2: To update the item

  • if we have found an item: we use it to make a new item

In step 3: To put the item back in the list (only if we found an item)

  • we loop through the list (again), and at the right id we replace the old with the new item

Hmm, this looks inefficient. We loop over our list twice. Once to retrieve the item, and once to update. Perhaps we could change our flow, to loop over the list only once, and when we are at the right id, do an update.

Do map — do not get+set

The function List.map, from the core List library, takes a List and turns it into another List. It has the following signature:

map : (a -> b) -> List a -> List b

The first argument is a function: (a -> b) that takes an item a from the list, and turns it into something else: b.

In our case a and b will both be an Item. We need a function to turn an existing item into a new item. Specifically, if the id of the item matches the one we are looking for, we return the item with a new name. If not, we simply return the original item.

Great! We just made our code about a third more compact.

Our new code loops over the list just once, and to each Item in the list, it applies the changeItemName function we defined.

Repetitive code

Now changing a name probably won’t be the only change we will need to make to an item. Let’s say we want to be able to increase the number of likes in our item too.

If we make another branch for this in our code, it would look very similar to the first branch:

This branch is different from the previous one only in a few places:

  1. There is no newName (or anything else) provided as an argument
  2. We change something else (the number of likes) in the item, by defining a function addItemLike
  3. Also, we now use the existing value item.likes to determine the new value

Everything else is pretty much the same:

  • We still check whether the item.id matches the id we provide
  • If it doesn’t match, we still return the unchanged item
  • We still do our List.map (albeit with a different function this time).

So perhaps there is room to reduce repetition even more?

Reduce repetition — round two

If we compare the two branches we made, the only parts really different are the lines that actually change the item:

-- to change the name
{ item | name = newName }
-- to increase the likes
{ item | likes = item.likes + 1 }

What if we could define a function, that has all the common parts of our branch, and takes an argument for the part where the two snippets are different.

We can. And it looks like this:

We specified here that one of the arguments is a function with a signature Item -> Item, which takes an item (the original one from the list, with matching id), and returns a new Item.

We put the (Item -> Item) between parentheses to let Elm know that we expect one argument here (a function). And not 2 separate Item arguments.

With this new function, we can now make our original code really compact:

Compile first, optimise later.

What’s really great about Elm, is that you can do these optimisations right in your normal workflow — with your code checked by the compiler all along the way.

The workflow in the example above is actually a very good and productive way to get things done in Elm:

  1. Build the code first time, and fix until it compiles.
  2. Then look for inefficiencies (double loops etcetera)
  3. Improve, and fix until it compiles.
  4. To make a new branch (or whatever), do copy-paste and change all bits that need change, and fix until it compiles.
  5. Look for duplicate code, and see where you can extract a helper function. (BTW, if you have two similar pieces of code, that’s fine. Wait until the similar code pops up a third of fourth time, and then extract the helper function).
  6. Extract the helper function, and fix until it compiles.

BTW: Do think about your data structures

In our example, the helper function updates an item in our List. One question was not addressed here: Is a List actually the most appropriate data structure for our purpose?

Elm also has a Dict data structure, where each item in the collection also has with a key. And Dict actually comes with a Dict.update function out of the box.

If you have a collection of items, and most of your operations are about individual items (retrieve, insert, delete, update), then probably a Dict is more suitable. With a Dict these kinds of operations are fast (especially in larger collections) and convenient.

If most of your operations involve the entire collection (like filtering, sorting, reordering, but also rendering), the a List is probably more suitable. And in those cases, a helper like we defined in the example would be useful for the (occasional) change to an item in the collection.

Learn from the libraries.

Do browse the Elm Core and Community libraries from time to time. There is a ton of great helper functions readily available for common patterns! E.g. the List.extra library has many additional functions for List, including updateAt and updateIfIndex, which work similar to the helper function we defined here.

There is also an excellent fancy search option that let’s you search for a specific function signature.

The libraries may help you spot common patterns in your own code. And you could look under the hood and browse the source code, to learn how to build your own helper functions.

Like what you read? Give Wouter In t Velt a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.