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 withEnum.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}
Top comments (0)