The Spooky Reduce Function

Tomas Barry
Butternut Engineering
8 min readNov 23, 2021
Photo by Sarah Shull on Unsplash

“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:

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 2
arr = [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 false
arr = [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
end
custom_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
end
custom_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
end
custom_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
end
custom_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".

--

--