18
Forever Functional: The mighty reduce, part 2
by author Federico Kereki
In our previous article we discussed how reduce()
is powerful enough to provide alternatives to other methods, such as map()
, filter()
, and reduceRight()
. We didn't cover, however, all the available higher-order functions that JavaScript provides. In this article we'll show how to emulate more methods:
-
every()
andsome()
methods that test if all or any of the elements of an array satisfies a condition; -
find()
andfindIndex()
that let you search an array for an element satisfying some condition, and -
flat()
andflatMap()
that create a new array by "flattening" (we'll see what this means) its elements, with an optional mapping function thrown in as well.
As in the other article, we'll see that all these methods can be emulated by properly using reduce()
. And, let's also hurry up and answer a question: should we do such emulation? The answer, again, will be that this is more an exercise in "lateral thinking" about finding new ways of doing things, and about learning more about JavaScript and programming in general. We may not end up using these alternative implementations, but the techniques that we'll use may help in other places.
The two .every()
and .some()
methods greatly simplify testing arrays for conditions.
-
every()
is true, if and only if every element of an array satisfies a given predicate; and -
some()
is true if there is at least one element in an array, which satisfies the given predicate.
Oh, an aside just for Boolean logic buffs: we may use the duality principle, and say that
-
some()
is false, if and only if no element of an array satisfies a given predicate
How can we implement .every()
and .some()
based on .reduce()
? The following ways are a possibility.
const every = (arr, fn) =>
arr.reduce((x,y) => x && fn(y), true);
const some = (arr, fn) =>
arr.reduce((x,y) => x || fn(y), false);
The first implementation ANDs together all the elements in the array, starting with a true
; the second ORs all elements, and starts with false
. Because of how JavaScript short-circuits the evaluation of boolean expressions, the fn()
predicate won't be evaluated again, once the value of .every()
or .some()
has been calculated. (For instance, if a value satisfies the predicate, the accumulator in .some()
will become true, and future evaluations of x || fn(y)
won't do the second part.) Let's test this, with the help of our addLogging()
higher order function from our previous article on higher order functions.
const arr = [22, 9, 60, 56, 12, 4];
const isOdd = (x) => x % 2 === 1;
every(arr, addLogging(isOdd));
// Enter isOdd 22
// Exit isOdd false
some(arr, addLogging(isOdd));
// Enter isOdd 22
// Exit isOdd false
// Enter isOdd 9
// Exit isOdd true
The isOdd()
function tests if its parameter is odd. If we try to see if all the elements in the arr
array are odd, the first test already fails, and we see that isOdd()
wasn't called again. If testing whether some elements of the array are odd, after testing 22 and 9 (when the test succeeded) no more calls to the predicate are done. If you wish to, however, think about how to also skip processing the rest of the array. Even if the predicate is no longer called, .reduce()
will still go through the whole array; a nice exercise!
Let's now turn to studying find()
and findIndex()
. The first one searches an array for the first element that satisfies a given predicate and returns it, or undefined
otherwise. The second one is similar but returns the index of the element, or -1 if none was found.
The implementation for the former is similar to that of every()
. We'll go through the array searching for a value that satisfies the condition, starting with an undefined
accumulator. When we find an appropriate element, we change the accumulator to the element. For the latter we'll do a similar thing, but we'll keep track of the position of the array, starting with -1.
const find = (arr, fn) =>
arr.reduce((x,y) => (x===undefined && fn(y) ? y : x), undefined);
const findIndex = (arr, fn) =>
arr.reduce((x,y,i) => (x===-1 && fn(y) ? i : x), -1);
You must remember that .reduce()
calls your reducing function providing the value of the accumulator, the current element of the array, and finally its index; the latter is the one that we'll keep in findIndex()
.
Debugging a web application in production may be challenging and time-consuming. OpenReplay is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue.
It’s like having your browser’s inspector open while looking over your user’s shoulder.
OpenReplay is the only open-source alternative currently available.
Happy debugging, for modern frontend teams - Start monitoring your web app for free.
The flat operation creates a new array, recursively concatenating all its elements (that may be arrays) up to a specified depth; by default, just 1. For example, imagine you query an API and it returns an array with the orders of a customer, and for each order you have an array of products: something like arr=[[22, 9, 60], [56], [12, 4]]
. (His first order had three products; the next, one; and the last, two.) We can flatten this into a single array by using flat()
: arr.flat()
would be [22, 9, 60, 56, 12, 4]
.
Flattening the array just one level (as we did in the previous paragraph) is simple. We could do it by spreading, but since we're focusing on the reduce()
method, let's go that way.
const flatOne = arr =>
arr.reduce((f, v) => f.concat(v), []);
We start with an empty array, and we concatenate each element of the array to it. If an element happens to be an array itself, we'll get it "flattened".
Let's now see how we can totally flatten an array, meaning that if some elements are arrays, whose elements are also arrays, and so on, all will get flattened. Recursion suits the problem naturally; each time we process an element of the array, if it's an array, we flatten it.
const flatAll = arr =>
arr.reduce((f, v) => f.concat(Array.isArray(v) ? flatAll(v) : v), []);
Given these two functions, we can now flatten an array at any desired number of levels. We'll use recursion again, and we get a nice implementation.
const flat = (arr, n = 1) =>
n === Infinity
? flatAll(arr) // [1]
: n === 1
? flatOne(arr) // [2]
: flat(flatOne(arr), n - 1); // [3]
According to the flatMap()
specification, if the level is Infinity, the whole array must be totally flattened, so that's what we do [1]. If we want to flatten it just one level, we use our previous function [2]. And, finally, if we want to flatten the array several levels, we flatten it one level, and we recursively flatten it more levels [3].
What about flatMap()
? OK, we'll cheat here: that operation is just a map()
followed by a flat()
-- and as we've already seen how these two operations can be emulated with the help of flat(), we don't have to do anything special here!
We have now completed studying how reduce()
may be certainly considered the most flexible and potent functional method for JavaScript programmers. Even if we don't use the implementations seen here, we've studied several techniques and algorithms that may form the base for our own coding. We've always been trying to work functionally, and we now know what's the mightiest tool for that: reduce()
!
18