The Spooky Reduce Function
“Reducing” an array seems to be one of those things that many engineers struggle with at first. So much so that they tend to avoid reductions even though they are comfortable with reduce’s friends (mapping, filtering, etc).
I hope that by the end of this article the idea of “reducing” an array has lost its spookiness and you feel brave enough to use a really powerful operation.
Array Methods
Regardless of your programming language of choice, there is typically some set of array utility methods to perform common operations on arrays. Things like “mapping”, “filtering”, “finding”, and “reducing” are the most popular methods. For reference, here are the array methods for Ruby and JavaScript:
- Ruby — 109 methods
- JavaScript — 38 methods
Different programming languages have different names for the same operations, and some even have two implementations of essentially the same method. Ruby is particularly bad for this. Ruby has filter
and reject
methods that can be used interchangeably to achieve the same goal (everything you can do with a filter
, you can do with a reject
). However, Ruby implements both of these methods as either can be used in specific contexts to allow for more declarative code. Consider the following example:
arr = [1, 2, 3, 4, 5]# Option 1
arr.filter { |el| el.even? }
# Option 2
arr.reject { |el| !el.even? }
Which option reads better to you? I would argue option 1 since it reads as “filter for all elements that are even” rather than potentially missing the !
in option two and accidentally reading it as "reject all elements that are even".
Option 1 can be made even more concise as arr.filter(&:even?)
thanks to Ruby’s lovely DSL (Domain Specific Language). I am also aware that we could actually use the reject here too, writing it as arr.reject(&:odd?)
, again thanks to Ruby’s lovely DSL but the above is only a simple example.
Key Take Away
You don’t need to know the entire set of array methods in your programming language of choice. But you should be aware of the key methods, what they are called and when using a new language you should keep in mind that they might be called something different but provide the exact same functionality.
Type Signatures
Let us now discuss every engineer’s favourite topic — type signatures. Understanding the type signature of common array methods can really highlight just how powerful they are. If you think about array methods as type signatures rather than implementations, you don’t need to consider how it is implemented. Even better is that by understanding the type signature, you’ll be able to pick up and use the array methods in any language you pick up.
Personal Opinion: know your type signatures. You will have a better understanding of the array methods and make better use of them.
I’m going to use a DSL that helps me to frame the type signatures in a language-agnostic way. This signature can be read as:
method: (param1, param2) => returnType
Map
The most commonly used and understood array method is “map”. This method takes one array and turns it into another:
map: ([a], (a) => b) => [b]
“Mapping” an array is simply turning one array into another array by performing some operation on every element of the input array. Therefore, the resultant array size will be the same as the array operated on (since we operate on every input array element).
An example:
arr = [1, 2, 3, 4, 5]# Ruby
# Type signature breakdown:
# arr : [a]
# |el| el * 2 : (a) => b
# result : [b]
result = arr.map { |el| el * 2 } # resultant array is [2, 4, 6, 8, 10]arr = [1, 2, 3, 4, 5]/**
* JavaScript
* Type signature breakdown:
* arr : [a]
* (el) => return el * 2 : (a) => b
* result : [b]
*/
result = arr.map((el) => el * 2) // resultant array is [2, 4, 6, 8, 10]
Filter
“Filtering” an array is another widespread operation. Given some array, you want to pluck only those elements that match some criteria:
filter: ([a], (a) => boolean) => [a]
Look familiar? The type signature is nearly identical to map
. A key difference between map
and filter
is that the operation returns a boolean
, and it is based on this value whether or not the element will be in the resultant array. A second key difference is that a map
returns an array that can be different to the input array, a filter
returns a subset of the input array (this is signified by the return value being of the same type as the input value [a]
):
arr = [1, 2, 3, 4, 5]# Ruby
# Type signature breakdown:
# arr : [a]
# |el| el % 2 == 0 : (a) => boolean
# result : [a]
result = arr.filter { |el| el % 2 == 0 } # resultant array is [2, 4]arr = [1, 2, 3, 4, 5]/**
* JavaScript
* Type signature breakdown:
* arr : [a]
* (el) => el % 2 === 0 : (a) => boolean
* result : [a]
*/
result = arr.filter((el) => el % 2 === 0) // resultant array is [2, 4]
Find
“Finding” on an array is simply the intention of plucking a single element from an array that matches some condition:
find: ([a], (a) => boolean) => a
Wouldn’t you know, the signature looks pretty familiar once again? The find
is a refined version of a filter
where instead of returning an array we only return a single element (a
instead of [a]
):
arr = [1, 2, 3, 4, 5]# Ruby
# Type signature breakdown:
# arr : [a]
# |el| el == 2 : (a) => boolean
# result : a
result = arr.find { |el| el == 2 } # result is 2arr = [1, 2, 3, 4, 5]/**
* JavaScript
* Type signature breakdown:
* arr : [a]
* (el) => el === 2 : (a) => boolean
* result : a
*/
result = arr.find((el) => el === 2) // result is 2
The find method generally returns the first element that matches the criteria. However, depending on the language you are using, the order of the array elements may not be immutable, and you might want first to sort the array.
Every/All
Programming languages also generally have some functionality to test whether all or every element of the array matches some condition:
every: ([a], (a) => boolean) => boolean
arr = [1, 2, 3, 4, 5]# Ruby
# Type signature breakdown:
# arr : [a]
# |el| el.even? : (a) => boolean
# result : boolean
result = arr.all? { |el| el.even? } # result is falsearr = [1, 2, 3, 4, 5]/**
* JavaScript
* Type signature breakdown:
* arr : [a]
* (el) => el % 2 === 0 : (a) => boolean
* result : boolean
*/
result = arr.every((el) => el % 2 === 0) // result is false
Key Take Away
Knowing the general type signatures of array methods can really help you. In the 4 array methods we have looked at so far, all of them have near-identical type signatures that can be summed up as:
Given some array, do something to it, and return something
The “spooky” one — reduce
As it turns out, the general statement above “Given some array, do something to it, and return something” is essentially the type signature of a reduce
function which can specifically be defined as "a function that takes an array and reduces it to a single value":
reduce: ([a], (a, acc) => acc, acc) => acc
acc is short for “accumulator”.
The reduce
function can be used to turn an array of elements into anything. Take an example where you have an array of dogs, and you want to get the split of breeds in the array of dogs. A reduce
is perfect for this:
dogs = [
{
name: 'Lucy',
breed: 'Jack Russell Terrier'
},
{
name: 'Floofer',
breed: 'Jack Russell Terrier'
},
{
name: 'Ace',
breed: 'Labrador'
},
{
name: 'Buster',
breed: 'Cavapoo'
}
]# Ruby
# Type signature breakdown:
# dogs : [a]
# acc[el[:breed]] =
# acc[el[:breed]].is_a?(Integer)
# ? acc[el[:breed]] + 1
# : 1
# acc : (a, acc) => acc
# {} : acc
# result : acc
result = dogs.reduce({}) do |acc, el|
acc[el[:breed]] = acc[el[:breed]].is_a?(Integer) ? acc[el[:breed]] + 1 : 1 acc
end# {
# "Jack Russell Terrier"=>2,
# "Labrador"=>1,
# "Cavapoo"=>1
# }dogs = [
{
name: 'Lucy',
breed: 'Jack Russell Terrier'
},
{
name: 'Floofer',
breed: 'Jack Russell Terrier'
},
{
name: 'Ace',
breed: 'Labrador'
},
{
name: 'Buster',
breed: 'Cavapoo'
}
]/**
* JavaScript
* Type signature breakdown:
* dogs : [a]
* if (acc[el.breed] > 0) {
* acc[el.breed] += 1
* } else {
* acc[el.breed] = 1
* }
* return acc : (a, acc) => acc
* {} : acc
* result : acc
*/
result = dogs.reduce((acc, el) => {
if (acc[el.breed] > 0) {
acc[el.breed] += 1
} else {
acc[el.breed] = 1
}
return acc
}, {})/**
* {
* Jack Russell Terrier: 2,
* Labrador: 1,
* Cavapoo: 1
* }
*/
Remember when we defined a reduce
as "a function that takes an array and reduces it to a single value"? Well, that "single value" can be anything, even another array. You might start to see that every array method is a specific implementation of the reduce
method. Let's look at how we can implement the array methods that we have discussed so far using reduce
:
Map
# Ruby
def custom_map(init_arr:, operation:)
init_arr.reduce([]) do |acc, el|
[*acc, operation.call(el)]
end
endcustom_map(init_arr: [1, 2, 3], operation: Proc.new { |el| el * 2 })// Javascriptconst customMap = (({ initArr, operation }) => {
return initArr.reduce((acc, el) => {
return [...acc, operation(el)]
}, [])
})customMap({ initArr: [1, 2, 3], operation: (el) => el * 2 })
The map
function hides away the acc
from us and initialises it as []
.
Filter
# Ruby
def custom_filter(init_arr:, operation:)
init_arr.reduce([]) do |acc, el|
if operation.call(el)
[*acc, el]
else
[*acc]
end
end
endcustom_filter(init_arr: [1, 2, 3], operation: Proc.new { |el| el % 2 == 0 })// Javascriptconst customFilter = (({ initArr, operation }) => {
return initArr.reduce((acc, el) => {
if (operation(el)) {
return [...acc, el]
} else {
return [...acc]
}
}, [])
})customFilter({ initArr: [1, 2, 3], operation: (el) => el % 2 === 0 })
The filter
function hides away the acc
from us and initialises it as []
.
Find
# Ruby
def custom_find(init_arr:, operation:)
init_arr.reduce(nil) do |acc, el|
if acc.nil? && operation.call(el)
el
else
acc
end
end
endcustom_find(init_arr: [1, 2, 3], operation: Proc.new { |el| el % 2 == 0 })// Javascriptconst customFind = (({ initArr, operation }) => {
return initArr.reduce((acc, el) => {
if (acc === null && operation(el)) {
return el
} else {
return acc
}
}, null)
})customFind({ initArr: [1, 2, 3], operation: (el) => el % 2 === 0 })
The find
function hides away the acc
from us and initialises it as null
.
Every
# Ruby
def custom_every(init_arr:, operation:)
init_arr.reduce(true) do |acc, el|
acc && operation.call(el)
end
endcustom_every(init_arr: [1, 2, 3], operation: Proc.new { |el| el % 2 == 0 })// Javascriptconst customEvery = (({ initArr, operation }) => {
return initArr.reduce((acc, el) => {
return acc && operation(el)
}, true)
})customEvery({ initArr: [1, 2, 3], operation: (el) => el % 2 === 0 })
The every
function hides away the acc
from us and initialises it as false
.
Key Takeaway
Let’s look back at the type signature for reduce
:
reduce: ([a], (a, acc) => acc, acc) => acc
You can think of a reduce
function simply as taking an array and doing something to each element of the array while you build up to some final value. This is the most powerful array method. It is the foundation of all other methods, and you can use it to do magical things.
I hope you no longer consider it "spooky".