Whilst taking the last Advent of Code as an opportunity to work on my Elixir skills, I quickly hit a roadblock. Although I was comfortable with Elixir’s syntax, I didn’t know how to write a loop! It was rather disconcerting to fall at such an early hurdle after having been a programmer for well over a decade.
It was partly the experience that was to blame: my mental model of working with collections was tightly coupled to
for-loop style implementations.
Although Elixir does have a
for construct it is not a drop-in replacement for imperative loops.
To see why, let’s say we want to create a new list of all even numbers found an input list of integers. An imperative approach might look like this:
all_numbers = [4, 2, 5, 9, 6, 1, 0] even_numbers =  for num in all_numbers: if num % 2 == 0: even_numbers.append(num) print(even_numbers) # [4, 2, 6, 0]
Naively translating this to Elixir:
all_numbers = [4, 2, 5, 9, 6, 1, 0] even_numbers =  for num <- all_numbers do if Integer.mod(num, 2) == 0 do even_numbers = [num | even_numbers] end end IO.inspect(even_numbers) # 
What gives‽ The list is empty after the loop.
As astute readers of the documentation we already know that data structures in Elixir are immutable.
That’s why we created a new list each iteration rather than ‘appending’ to the same list by doing
[num | even_numbers].
But we could pretend Python lists are immutable and that example would still work.
all_numbers = [4, 2, 5, 9, 6, 1, 0] even_numbers =  for num in all_numbers: if num % 2 == 0: even_numbers = [num] + even_numbers print(even_numbers) # [0, 6, 2, 4]
This confusion is unrelated to concepts of ‘immutability’ or being ‘functional’: it’s a result of Elixir’s different scoping rules.
Similarly to Python scopes1, a scope in Elixir cannot modify the variables in outer scopes.
Unlike Python however Elixir introduces additional scopes within blocks such as
This means our assignment to
even_numbers inside the Python
for is ‘visible’ outside the loop (the scopes are the same) whereas it is not visible outside in Elixir (the scopes are different).
The difference in scoping is enough to rule out many of our usual loop-writing techniques.
How can we approach iteration to fit within the Elixir model?
Approach #1: the
It’s a bit dry, but simply reading through the
Enum module documentation from top to bottom is superb for discovering ways of rewriting a loop.
We might expect the obvious things like
Enum.map/2, but there are other interesting characters in there.
I found these helpful in the Advent of Code challenges:
Enum.chunk_by/2: groups a collection into lists whenever a categoristion function changes its return value.
Enum.flat_map/2: maps a function and flattens the final list. Useful if an invocation of the mapping function returns multiple elements.
Enum.frequencies_by/2: counts groups of elements for which the key function returns the same value.
Enum.split_while/2: splits the list in two based on the return value of the predicate.
The idea of these kinds of functions might be familiar to you if you’re an old hand with Python’s itertools, which offers some similar functionality.
Enum is an essential tool rather than a optional extra.
In our example, we can think of the task as either rejecting all odd numbers, or keeping only even numbers.
In either case, we are filtering the list, and so
Enum.filter/2 is appropriate.
Enum.filter(all_numbers, fn num -> Integer.mod(num, 2) == 0 end) # [4, 2, 6, 0]
With a bit of refactoring we can make this wonderfully readable.
require Integer Enum.filter(all_numbers, &Integer.is_even/1) # [4, 2, 6, 0]
It’s great that
Enum provides so much, and it’s often all we need.
But what if we need something bespoke, aren’t we back to square one?
And if we can’t write our usual imperative loops, how does
Enum provide the same behaviour anyhow?
Approach #2: reduce
The initial value of the accumulator is
acc. The function [
fun] is invoked for each element in the enumerable with the accumulator. The result returned by the function is used as the accumulator for the next iteration. The function returns the last accumulator.
The term ‘accumulator’ comes from a common usecase for reduce: summation.
Enum.reduce([1, 2, 3], 0, fn num, acc -> acc + num end) # 6
We accumlate numbers from the list during each iteration to form a running total. When the list is exhausted that running total is the sum of all the numbers.
But the word ‘accumulator’ may be too restrictive. The value of the accumulator can be any object: not just a number but another list, a tuple, a struct, and so on. This realisation was a bit of a revelation for me!
I prefer to think of the accumulator more broadly as state, passed from one iteration to the next.
In this way we can turn an imperative loop into a
reduce form by simply storing any information we need to mutate from ‘outside’ the loop in the accumulator.
Enum.reduce(all_numbers, , fn num, even_numbers -> if Integer.mod(num, 2) == 0 do [num | even_numbers] else even_numbers end end) |> Enum.reverse() # [4, 2, 6, 0]
In our example the ‘state’ between interations is the list of all even numbers encountered so far. At the end of the iteration, the state is the complete even-number list.
By using reduce and setting the accumulator to be the inter-iteration state, we can easily rewrite most imperative loops. It’s a powerful tool!