Published on 7 September 2023
for
loops in order to process an array, mutating it along the way, accessing individual elements with cryptically named i
s and j
s and spending an hour trying to figure out what exactly happens inside a three-nested loop code block..map
, .filter
, .sort
and .reduce
, is much easier. Not only to write, but also to convey the intent of what is meant in a more concise and understandable way. While it may take one a while to process a for
loop which only doubles all the numbers in an array, with the use of .map
it can simply be written as array.map(x => x * 2*)
..map
takes an array, and returns an array with each element of the original array ran through the iteratee." That's quite a mouthful, and keeping all of that in mind when thinking about a solution to a problem is not that easy, especially since at first one tends to recoil and jump back to the more familiar imperative mode of thinking with loops and indices..map
and .sort
.const numbers = [1, 2, 3]
const doubledNumbers = numbers.map(x => x * 2) // [2, 4, 6]
.map
allows for dividing the problem of processing an array into two distinct steps. First, we recognise that each element must be processed individually. Then, we specify how to do it, having to think only about one element at a time.const users = [
{ name: 'Anna', surname: 'Komnene' },
{ name: 'Michael', surname: 'Psellos' },
// ...
]
const joinName = user => `${user.name} ${user.surname}`
users
array via .map
:const usernames = users.map(joinName)
.map
gives us some guarantees. We do not have to delve into the iteratee to be guaranteed that the elements are not in a different order, or that the length of the output array is different than the input. The only thing that has changed are the values of the elements of the input array..sort
is similarly straightforward, and here, too, we can say with certainty that the length of the array has not been changed. In this case we also know that the values are the same and the only thing that's been impacted is the order. The only larger trouble with .sort
is having to remember the ternary state of the comparator function. Remembering that using (a, b) => a - b
will result in numbers being sorted in an ascending order is usually enough to get to a solution.const numbers = [3, 1, 2]
const sortedNumbers = numbers.sort((a, b) => a - b) // [1, 2, 3]
.filter
results in an array of equal or shorter length, .find
(and its siblings, .findLast
, .findIndex
and .findLastIndex
) comes up with just one item of the array, while .some
and .every
return a mere boolean value.const numbers = [1, 2, 3, 4, 5, 6]
const isEven = n => n % 2 === 0
const evenNumbers = numbers.filter(isEven) // [2, 4, 6]
const two = numbers.find(isEven) // 2
const hasEven = numbers.some(isEven) // true
const allEven = numbers.every(isEven) // false
.filter
, there also are certain guarantees - the output array will not have its individual values modified or their order changed, the only thing that can happen is that some will be missing..flatMap
we might end up with more or less items, however we are guaranteed to end up with an array. This is not the case with .reduce
, which collapses the entire collection to a single value (which, well, can also be an array that's even longer)..flatMap
especially is quite daunting at first glance - why exactly would we need to have it? The way it's often explained doesn't make sense either - it maps and then flattens, but why is that so important that we need to have a separate function for it in the JS spec?.flatMap
is that it gets around the limitation of .map
being allowed to map one input element to exactly one output element. .flatMap
can transform a single input element into zero, one, or more output elements. That way we can, for example, implement a filter in terms of .flatMap
.const numbers = [1, 2, 3, 4, 5, 6]
const evenNumbers = numbers.flatMap(n => isEven(n) ? [n] : []) // [2, 4, 6]
.flatMap
we can filter and map at the same time. An example - a validator that transforms a list of values into a list of errors.const numbers = [1, 2, 3, 4, 5, 6]
const errors = numbers.flatMap(n => !isEven(n)
? [`${n} is not even.`]
: []
)
// ['1 is not even', '3 is not even', '5 is not even']
.flatMap
will commonly work for problems to which one's first intuition is to reach for a mutable array, .push
some items and then return it at the end..reduce
allows for collapsing an entire array to a single value. The value can be anything - a primitive value, or even something more robust like an array or an object. What's interesting is that we can reimplement pretty much every single other array method in terms of .reduce
.(accumulator, value) => newAccumulator
which is repeatedly applied to the elements of the array, much like .map
, however it also carries with it an accumulator argument. In many other languages a similar function is called fold
which also illustrates its purpose.const numbers = [1, 2, 3]
const add = (a, b) => a + b
const sum = numbers.reduce(add, 0)
.reduce
. The reducer can take two values of the same type - in which case it exploits the monoidal nature of arrays, as above - or two values of different types, in which case it might be better to rewrite it first with a .map
. An example of this can be a function converting an array to a map.const users = [
{ id: 'abc', name: 'Anna', surname: 'Komnene' },
{ id: 'def', name: 'Michael', surname: 'Psellos' },
// ...
]
const usersMap = users.reduce(
(map, { id, ...user }) => map.set(id, user),
new Map()
) // Map{'abc' => {...}, 'def' => {...}, ...}
const usersEntries = users.map(({ id, ...user }) => [id, user])
const usersMap = new Map(usersEntries)
.reduce
to write for
loop-like constructs, but that's rarely a good idea..map
is an implementation of a functor's fmap
, .flatMap
is an implementation of a monad's bind
and .reduce
is an implementation of a monoid's concat
. But even without knowing about the category theory behind arrays, it is useful to think about the array methods just in terms of the number of elements that they affect.