Deep understanding of Enum.map_reduce/3 in Elixir

Today I found a function in Elixir’s standard lib that I have often needed.

Introducing: Enum.map_reduce/3

Enum.map_reduce/3 can replace Enum.reduce/3 when the reduce maps each element to another element and we also want to maintain some state or build a result along the way.

This might be a bit cryptic, so let’s start with an example.

Have a cache while mapping

Let’s pretend I don’t know about Enum.map_reduce/3 and I need to map a list of numbers to the maximum of the number and the previous number.

I would do something like this:

input = [5, 2, 8, 9, 1, 2, 7, 2]

{_last, numbers} =
  Enum.reduce(input, {0, []}, fn num, {last, acc} ->
    {num, [max(num, last) | acc]}
  end)

Enum.reverse(numbers)

(Ok, this simple example could also be solved nicely with Enum.chunk_every/2 and Enum.map/2, but for the sake of example let’s play with this)

This will give the expected result [5, 5, 8, 9, 9, 2, 7, 7].

Notice that I need to reverse the number list at the end, since I append numbers to the start of the list.

If only there is a way to do this in a smarter way…

Well, with Enum.map_reduce/3 we can do this:

input = [5, 2, 8, 9, 1, 2, 7, 2]

{numbers, _last} =
  Enum.map_reduce(input, 0, fn num, last -> {max(num, last), num} end)

So here we use map_reduce‘s accumulator to remember what the last number was.

Build a result while mapping

We can also use it to accumulate something we need as a result while we’re mapping a list.

Perhaps we want to know the largest number while doing the max-thing above:

{numbers, {max, _last}} =
  Enum.map_reduce(input, {0, 0}, fn num, {max, last} ->
    {max(num, last), {max(num, max), num}}
  end)

Which will give the same numbers as before, but now we also get 9 as the max item.

Enum.flat_map_reduce

In the cases where each iteration might yield zero or many items to the resulting list, we can use Enum.flat_map_reduce/3. This works like Enum.map_reduce/3 with two differences:

  • Each iteration must yield a list of elements instead of a single element. Notice that we can use an empty list if an iteration should not add something to the resulting list.
  • It is possible to break early using :halt, almost like with Enum.reduce_while/3

Example: We want to map a list to another list where:

  • All odd numbers are removed
  • All even numbers are doubled
  • We want to sum the original numbers
  • If we hit 7, we stop
input = [5, 2, 8, 9, 1, 2, 7, 2]

Enum.flat_map_reduce(input, 0, fn num, sum ->
  case num do
    7 -> {:halt, sum}
    n when rem(n, 2) == 0 -> {[n, n], sum + n}
    n -> {[], sum + n}
  end
end)

Which will give {[2, 2, 8, 8, 2, 2], 27}

Leave a Reply

Your email address will not be published. Required fields are marked *