What is reduce and why should I care?
Functional programming is in many cases tied to immutability and lack of
for
-loops that are available in the procedural paradigm. Instead we’re told to
use concepts like recursion or the more high level toolkit that belongs of
map
, filter
and similar functions.
While map
for example is a very good tool which should be used as often as
possible it just simply can’t do certain kinds of tasks. As an example map
is
strictly used for 1:1 transformations where the amount of items in the list
we’re performing the map on will be the same in the result. To get around that
problem we could potentially do something like this:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|> Enum.map(fn (item) ->
if item % 3 != 0 do
item * 2
else
nil
end
end) # [1, 4, nil, 8, 10....]
|> Enum.reject(fn (item) -> item == nil end)
#=> [2, 4, 8, 10, 14, 16, 20]
This solves the problem 100% but it’s also an operation that forces us to iterate and perform another step which not only causes us to cycle the list twice, which means that for every item in the initial list, we’ll require double the amount of time. This is not an issue if we’re working with smaller datasets since twice the amount of time on almost nothing is still almost nothing.
Enter reduce
The reduce
function is what we can call a lower level abstraction of most of the
functions we’re using to perform transformations on a list. In fact, you can use
reduce
to reimplement most other functions available on lists/arrays but you
can’t do it the other way around. So how does it work?!
In order to understand reduce
it helps to first first think of data as
immutable, once it’s defined it might not be changed.
When we’ve accepted the fact that we cannot ever change anything we’ll come to
the realization that in order for us to perform transformations, our only chance
is by creating new things. This is exactly what reduce
is about, we’re using
the knowledge of something we have, in order to create something new, one step
at a time. Summation is a common example of how we can transform a list of items
into something else, let’s use reduce to subtract money with multiple currencies
from a bank account instead:
purchases = [
%Money{amount: 5, currency: :SEK},
%Money{amount: 9, currency: :USD},
%Money{amount: 1, currency: :SEK},
%Money{amount: 3, currency: :USD},
%Money{amount: 7, currency: :USD},
]
bank_account = %BankAccount{
SEK: 5000,
USD: 127,
}
Enum.reduce(list, bank_account, fn (purchase, bank_account) ->
IO.inspect({bank_account, purchase})
purchase_currency = purchase.currency
amount_after_purchase = bank_account[purchase.currency] - purchase_currency
if amount_after_purchase < 0 do
raise "Not enough funds on your bank account"
end
%BankAccount{bank_account | ^purchase_currency: amount_after_puchase}
end)
If we were to run this code we’d get logs that states the current value of the bank account in each iteration and the purchase that is going to be processed:
# Data for processing first purchase
{%BankAccount{SEK: 5000, USD: 127}, %Money{amount: 5, currency: :SEK}}
# Before processing second purchase
{%BankAccount{SEK: 4995, USD: 127}, %Money{amount: 9, currency: :USD}}
{%BankAccount{SEK: 4995, USD: 118}, %Money{amount: 1, currency: :SEK}}
{%BankAccount{SEK: 4994, USD: 118}, %Money{amount: 3, currency: :USD}}
{%BankAccount{SEK: 4994, USD: 115}, %Money{amount: 7, currency: :USD}}
# Lastly the last operation will be processed and the bank account will be
# returned since that is the value we're transforming into.
# => %BankAccount{SEK: 4994, USD: 108}
So that is how we can use reduce to do a form of summation (we’re using the bank account as the initial value and applying a list of data with the help of a reducer function).
Other use cases
reduce
isn’t always about reducing a list of values into one value, it could
also be about performing business logic in an efficient matter. As an example
it’s quite common that we’d like to perform multiple blocks of logic onto a list
of items. Let’s consider the case where we’d like to group a list of people by
the first letter of their last name but only show adults in the result:
persons = [
%Person{first_name: "Eli", last_name: "Wood", age: 31},
%Person{first_name: "Karina", last_name: "Becker", age: 17},
%Person{first_name: "Albert", last_name: "Horton", age: 22},
%Person{first_name: "Margaret", last_name: "Winston", age: 67},
]
persons
Enum.reduce(%{}, fn (person, grouped_persons) ->
if person.age =< 18 do
grouped_persons
else
last_name_initial = String.at(person.last_name, 0)
%{grouped_persons | ^last_name_initial: (grouped_persons[last_name_initial]
}} []) ++ [person]}
end
end)
With reduce we’re able to do both things in one pass over the list instead of
two, which we could have done by using filter
and group_by
.
Closing words
While reduce
isn’t necessary in most cases (especially where performance
doesn’t matter) it can for the most part be replaced with functions such as
map
, filter
, group_by
, sum
, min
, max
. Since those are higher level
and their descriptive nature makes them good options for both readability and
maintainability.
However there might be cases where the problem requires a more flexible tool or
where milliseconds can be the difference being a good and a bad experience.
For those cases it could be good to know how you can utilize the powerful reduce
to be able to reach your goals!