Reduce by example; much more than sum

Map, reduce and filter have been getting serious limelight in the blogosphere. Rightly so as they are powerhouses of any JavaScript developer’s toolkit but my favourite of the three is, without a doubt, reduce.

For the unenlightened: reduce is a method of an array that takes a function and an optional initial value to return a single value, hence reducing. A common example is the sum of several numbers:

Sum

var total = [23,12,32,3].reduce(function (sum, next) {
return sum + next;
}, 0);
total === 70

So why do I prefer it over map and filter? Firstly because it can be used to make the other two:

Map

function map(array, callback) { 
return array.reduce(function (accumulator, next) {
return accumulator.concat(callback(next));
}, []);
}

Filter

function filter(array, predicate) { 
return array.reduce(function (accumulator, next) {
return predicate(next) ? accumulator.concat(next): accumulator;
}, []);
}

In this post, the point I want to get across is that reduce is often typecast as just returning a numeric value or as a string builder but it can be so much more. What better way to show its power and simplicity than by example:

Examples

Indexing an array of objects by a property

A possible scenario is an array of people whom you want to index by name

function indexBy(items, key) {
return items.reduce(function (accumulator, item) {
accumulator[item[key]] = item;
return accumulator;
}, {});
}

Finding the closest date to a given date

Arguably one could sort the dates based on the difference and take the first item but sort mutates the existing array and there is the performance cost incurred by having to reposition elements.

function dateDifference(dateA, dateB) {
return Math.abs(dateA.getTime() - dateB.getTime());
}
function closestDate(to, dates) {
return dates.reduce(function (closest, next) {
return dateDifference(to, next) < dateDifference(to, closest) ?
next : closest;
});
}

Counting the matches in an array against a predicate

This could also be achieved with filter and then .length on the array however I appreciate that in a reduce you don’t create an array simply to calculate its length.

function matches(array, predicate) {
return array.reduce(function (count, next) {
return predicate(next) ? count + 1 : count;
}, 0);
}

Flattening arrays (single depth)

function flatten(arrays) {
return arrays.reduce(function (result, array) {
return result.concat(array);
}, []);
}

Filtering keys from an object

function filterKeys(target, keys) {
return Object.keys(target).reduce(function (result, key) {
if(keys.indexOf(key) === -1) {
result[key] = target[key];
}
return result;
}, {});
}

Joining strings by alternating separators

function join(strings, separators) {
return strings.reduce(function (result, next, idx) {
var nextSeparator = separators[(idx-1) % separators.length];
return result + nextSeparator + next;
});
}

Running promise-returning functions in series/waterfall

function series(promiseFns, initialValue) {
return promiseFns.reduce(function (prev, next) {
return prev.then(next);
}, Promise.resolve(initialValue));
}

Conclusion

As you can see, even complex scenarios can be made into quite elegant solutions with the help of a reduce function. It can be difficult to know when a reduce function is more appropriate over map, filter and even forEach, when performing an operation over an array; experience really helps with this but in the mean time here’s the mental checklist I use:

  • Perform a side-effect and don’t care about the result — forEach
  • Transform all elements — map
  • Remove elements — filter
  • Return a single primitive — reduce
  • Return an object — reduce
  • Return a different sized array — reduce
  • None of the above — probably reduce

NB: I’ve not covered the various other higher-order array methods in this post such as some, find, sort as they either mutate or are so specific I don’t feel like they have as much overlap. This does not mean that they are not worth learning.