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

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.

Elixir for Business: 5 Ways It Transforms Your Processes
Elixir for Business

Elixir for Business: 5 Ways It Transforms Your Processes

Learn how Elixir can improve business performance by reducing costs, enhancing efficiency, and speeding up time to market.

Elixir Tutorials
Elixir tutorials

Elixir Tutorials

Paweł Długosz explores how Elixir’s concurrency model and fault-tolerant design simplify scalable system development for developers