Reduce, Reuse... Refactor: Clearer Elixir with the Enum Module

Reduce, Reuse… Refactor: Clearer Elixir with the Enum Module

“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, other Enum 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 custom credo check I’ve created, which can help you identify places where Enum.reduce could be replaced with a simpler option.

Readability

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.

Performance

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.

Good Opportunities for 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:

Find Cases in Your Own Code with 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, []}

Keep reading

Common MVP mistakes: How to build smart without overbuilding
Common MVP mistakes: How to build smart without overbuilding

Common MVP mistakes: How to build smart without overbuilding

Learn how to avoid common MVP mistakes that slow you down and burn resources. Build smart, validate early, and grow with confidence.

Looking Forward to ElixirConf EU 2025
Looking Forward to ElixirConf EU 2025

Looking Forward to ElixirConf EU 2025

Rhys Davies previews ElixirConf EU 2025, highlighting keynotes, tech deep dives, and community stories set to unfold in Kraków this May.

Erlang Solutions’ Blog round-up

Erlang Solutions’ Blog round-up

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.