Reduce, Reuse… Refactor: Clearer Elixir with the Enum Module
- Brian Underwood
- 24th Apr 2025
- 8 min of reading time
“When an operation cannot be expressed by any of the functions in the Enum module, developers will most likely resort to reduce/3.”
From the docs for Enum.reduce/3
In many Elixir applications, I find
Enum.reduce
is used frequently.Enum.reduce
can do anything, but that doesn’t mean it should. In many cases, otherEnum
functions are more readable, practically as fast, and easier to refactor.I would also like to discuss situations that are a good fit for
Enum.reduce
and also introduce you to a customcredo
check I’ve created, which can help you identify places whereEnum.reduce
could be replaced with a simpler option.
Here are a few common reduce patterns—and their simpler alternatives. For example, here’s something I see quite often:
Enum.reduce(numbers, [], fn i, result -> [i * 10 | result] end)
|> Enum.reverse()
This is a situation that the Enum.map
function was designed for:
Enum.map(numbers, & &1 * 10)
Perhaps you know about Enum.map
, but you might see a call to reduce
like this:
Enum.reduce(numbers, 0, fn number, result -> (number * 2) + result end)
Let me introduce you to Enum.sum_by
!
Enum.sum_by(numbers, & &1 * 2)
Let’s look at something a bit more complex:
Enum.reduce(numbers, [], fn item, acc ->
if rem(item, 2) == 0 do
[item * 2 | acc]
else
acc
end
end)
|> Enum.reverse()
This is a perfect case for piping together two Enum
functions:
numbers
|> Enum.filter(& rem(&1, 2) == 0)
|> Enum.map(& &1 * 2)
Another option for this case could even be to use Enum.flat_map
:
Enum.flat_map(numbers, fn number ->
if rem(number, 2) == 0 do
[number * 2]
else
[]
end
end)
This is a decent option, but while this achieves the purpose of both filtering and mapping in a single pass, it may not be as intuitive for everybody.
Lastly, say you see something like this and think that it would be difficult to improve:
Enum.reduce(invoices, {[], []}, fn invoice, result ->
Enum.reduce(invoice.items, result, fn item, {no_tax, with_tax} ->
if Invoices.Items.taxable?(item) do
tax = tax_for_value(item.amount, item.product_type)
item = Map.put(item, :tax, tax)
if Decimal.equal?(tax, 0) do
{no_tax ++ [item], with_tax}
else
{no_tax, with_tax ++ [item]}
end
else
{no_tax, with_tax}
end
end)
end)
But this is just the same:
invoices
|> Enum.flat_map(& &1.items)
|> Enum.filter(&Invoices.Items.taxable?/1)
|> Enum.map(& Map.put(&1, :tax, tax_for_value(&1.amount, &1.product_type)))
|> Enum.split_with(& Decimal.equal?(&1.tax, 0))
Aside from improving readability, splitting code out into pipes like this can make it easier to see the different parts of your logic. Especially once you’ve created more than a few lines of pipes, it becomes easier to see how I can pull out different pieces when refactoring. In the above, for example, you might decide to create a calculate_item_taxes
function which takes a list of items and performs the logic of the Enum.map
line.
You may have already thought of a counterpoint: when you pipe functions together, you end up creating new lists, which means more work to be done as well as more memory usage (which means more garbage collection). This is absolutely true, and you should be thinking about this!
But I find that 99% of the time, the data I’m working with makes the performance difference negligible. If you find that your code is slow because of the amount of data that you need to process, you might try using the Stream
module — it has many of the same functions as Enum
, but works lazily. If that doesn’t work, then by all means, create a reduce
(and maybe put it into a well-named function)!
As Joe Armstrong said:
“Make it work, then make it beautiful, then if you really, really have to, make it fast.”
For some information about benchmarks that I’ve run to understand this better, see this analysis and discussion.
Enum.reduce
Aside from occasional performance reasons, Enum.reduce
can often be the simplest solution when you want to transform a data structure over a series of steps. For example:
credo_unnecessary_reduce
Remember that no one pattern works in all cases, so know what tools you have available! If you’d like to quickly find instances for potential improvements in readability, I built a Credo check to help spot where reduce can be swapped for something simpler.
You can drop it into your project and start catching these anti-patterns automatically.
https://github.com/cheerfulstoic/credo_unnecessary_reduce
Simply add it to your mix.exs
file:
{:credo_unnecessary_reduce, "~> 0.1.0"}
…and then enable it in your .credo.exs
file:
{CredounnecessaryReduce.Check, []}
Catch up on the latest from Erlang Solutions. This blog round-up covers key tech trends, including big data, digital wallets, IoT security, and more.
Learn how Elixir can improve business performance by reducing costs, enhancing efficiency, and speeding up time to market.
Paweł Długosz explores how Elixir’s concurrency model and fault-tolerant design simplify scalable system development for developers