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:
- Get the relevant item from the list
- Update the item
- 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:
- There is no
newName
(or anything else) provided as an argument - We change something else (the number of likes) in the
item
, by defining a functionaddItemLike
- 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 theid
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:
- Build the code first time, and fix until it compiles.
- Then look for inefficiencies (double loops etcetera)
- Improve, and fix until it compiles.
- To make a new branch (or whatever), do copy-paste and change all bits that need change, and fix until it compiles.
- 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).
- 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.