Russian translation: Уменьшение в JavaScript
When interviewing candidates,
I used to ask them about several native methods of Array in JavaScript
they had used and let them implement one of the methods on paper,
such as some
or map
.
If someone knows that functions can be passed around,
this should not be a hard task.
The reduce
function was most rarely mentioned.
The discussion
Last week Sophie Alpert tweeted about
her rule for the reduce
function. There was then a hot discussion,
as the last two rules in the list were controversial among many people.
my rule for .reduce(): only use it when the result has the same type as the items and the reducer is associative, like
— Sophie Alpert (@sophiebits) February 22, 2019
.reduce((a, b) => a + b, 0)
✅ summing some numbers
✅ multiplying some numbers
🚫 building up a list or object
🚫 just about anything else (use a loop)
I myself use this function a lot. Not only for summing and multiplying numbers, but also for composing a new array or object. Sophie's rule made me think about this function again.
Reduce vs Fold
Reduce
is a higher-order function which is usually called fold
in functional programming languages.
For me, fold
is definitely
a better name compared to reduce
,
since this's how it behaves like folding a fan in my head.
It also has directions, reduce
traverses a list from left to right,
reduceRight
does from the other direction.
You've probably seen foldl
and foldr
somewhere else,
they're the same thing but much readable.
In JavaScript, if no initial value is supplied to the reduce
function,
the first element in the given array will be used.
[1, 2, 3].reduce((a, b) => a + b, 0);
// The 1st element is used as initial value
// when the 2nd argument is missing.
[1, 2, 3].reduce((a, b) => a + b);
That's why I prefer foldl1
or foldr1
in other languages like Haskell,
which explicitly uses the first or last element as initial value.
foldl (+) 0 [1, 2, 3]
-- More intuitive
foldl1 (+) [1, 2, 3]
Two tasks
There are two simple tasks by given the following data, and solutions with normal loops:
const scoreArray = [
{ name: 'Jim', score: 99 },
{ name: 'Han', score: 55 },
{ name: 'Tom', score: 87 },
{ name: 'Ana', score: 50 }
];
a) Get the total score of all items.
function getScoreSum(array) {
let ret = 0;
array.forEach(item => {
ret += item.score;
});
return ret;
}
b) Get the item with the highest score.
function getHighest(array) {
let ret = {};
array.forEach(item => {
if (!ret.score || (ret.score < item.score)) {
ret = item;
}
});
return ret;
}
Seeing patterns
The above two functions have something in common:
- An initial value.
- A rule on how to update the initial value with each item in the list.
- Returns the final updated value.
We can define a function to abstract the repeated pattern.
This is actually a simple version of Array.prototype.reduce
.
function reduce(array, rule, initial) {
// initialization
let result = initial;
// traverse through the list and update the result
// according to the custom rule
array.forEach(item => {
result = rule(result, item);
});
// return the final result
return result;
}
Now rewrite the solutions to see how they will look like with reduce
.
function getScoreSum(array) {
return reduce(array, (ret, item) =>
ret + item.score, 0
);
}
function getHighest(array) {
return reduce(array, (ret, item) =>
ret.score > item.score ? ret : item, {}
);
}
Once you identify this pattern, the code will be more understandable. The benefit of the abstraction is that the rule can be separated and possibly be reusable.
function maxByScore(a, b) {
return a.score > b.score ? a : b;
}
function getHighest(array) {
return reduce(array, maxByScore, {});
}
The difference
The above getScoreSum()
and getHighest()
also have a different place.
getScoreSum()
returns a number.getHighest()
returns an object, the type is same with items in the list.
Normally it's called asymmetrical in its types when the type of the returned value is different from the type of items in the list. Otherwise, it is symmetrical.
The first two rules in Sophie's tweet basically referred to symmetrical functions.
.reduce((a, b) => a + b, 0);
.reduce((a, b) => a * b, 1);
Sometimes it's convenient to make the first element as the initial value when it's symmetrical.
Also, we could reduce one iteration in the loop. So here are little modifications to the reduce
function we just defined.
function reduce(array, rule, initial) {
let hasInitial = arguments.length > 2;
// make the first element as initial value
// when the initial argument is missing
let result = hasInitial ? initial : array[0];
let start = hasInitial ? 0 : 1;
for (let i = start; i < array.length; ++i) {
result = rule(result, array[i]);
}
// return the final result
return result;
}
Much simpler now:
function getHighest(array) {
return reduce(array, (a, b) => a.score > b.score ? a : b);
}
Building up a list
A good use case of building lists with reduce
is the implementation of the flat
or flatMap function.
const nestedArray = [
1, 2, 3, [4, 5], 6
];
With the normal loop:
function flat(array) {
let ret = [];
array.forEach(n => {
ret = ret.concat(n);
});
return ret;
}
Again, we see the pattern exposes.
So let's try to do it with reduce
.
function flat(array) {
return array.reduce((ret, n) => ret.concat(n), []);
}
As you can see it's not that bad. We can go further to support flattening on deep nested array:
const nestedArray = [
1, 2, 3, [4, 5],
[[6, 7], 8], 9, 10
];
function flat(array) {
return array.reduce((ret, n) =>
ret.concat(Array.isArray(n) ? flat(n) : n), []
);
}
Other examples
Some other functions can also be implemented with reduce
easily.
We can do this as a practice regardless of some performance concerns.
function map(L, fn) {
return L.reduce((acc, n, i) => [...acc, fn(n, i, L)], []);
}
function reverse(L) {
return L.reduce((acc, n) => [n, ...acc], []);
}
function join(L, sep='') {
return L.reduce((a, b) => a + sep + b);
}
function length(L) {
return L.reduce(acc => acc + 1, 0);
}
Thoughts
Abstractions are ways of thinking.
If the performance is not crucial I still prefer to use reduce
function whenever appropriate.
Because it allows me to focus on the most important part and write less code.