The concept of "Reducing" data
For this introduction I will use an imaginary music library (consisting of tracks and playlists) application for the context of examples. The underlying concepts also apply to any other kind of application.
When operating on data, it is often necessary to have knowledge about the data in a more abstract form than just the plain elements. In our music library you might want to know how many different titles of a single artist exist, or how many artists are in your library. You might also need to know all tracks being used in one or more playlists. You also might want to display how much hours of music the library (or a playlist) contains.
These are all typical use cases for reduce. (in general all kinds of counting, filtering, grouping, categorizing, unifying, concatenating data can be solved by applying reduce. You can even abuse reduce for mapping data.)
Be aware: Sorting data with reduce won't work out fine and is nowhere near the makes-sense horizon.
How reduce
works
A reducer function is in general a function that takes a accumulator (this is an object or value that you can think of as a state that is the result of the previous call to your reducer function) and a data element as parameters and returns a new value/object:
(accumulator, element) => resultValue
What makes reduce
very special - in the scope of functional programming - is that it has a state. Not the reducer function itself (this one is ideally stateless - to make it easy to test and easy to reuse across your project), but the process of reducing the data itself. Each accumulator
value gives you the intermediate result of the previous step.
The concept of reducing is especially applied in Array.prototype.reduce()
(thats probably also the reason for its name). Another well known use of reduce is in the Redux state management library. It uses reducer functions in order to change the state in a reproduceable way using an action and the current state. The current state is passed as the accumulator
and the action is the element
parameter being passed to the reducer.
Back to array.reduce()
Array.prototype.reduce()
(or array.reduce()
of every Javascript instance of Array in JS) iterates over every element of its instance array, starting on the left side (array[0]
) of the array, calling your reducer for each element and the result of the reducer for the previous element.
const array = [ 1, 2, 3, 5 ];
const finalResult = array.reduce((accumulator, element) => {
console.log("accumulator: ", accumulator);
console.log("element: ", element);
const result = accumulator + element;
console.log("result: ", result);
return result;
});
console.log("final result: ", finalResult);
// Console output:
//
// > accumulator: 1
// > element: 2
// > intermediate result: 3
// > accumulator: 3
// > element: 3
// > intermediate result: 6
// > accumulator: 6
// > element: 5
// > intermediate result: 11
// > final result: 11
//
The example above seems to be just fine. With a closer look, we can find a fundamental problem, that makes writing more complex reducers not practicable:
The first value that is passed to the element
parameter of our reducer is actually the second value in the array while the actual first array value is passed to as the accumulator
value at the first invocation.
Why is this a problem?
For more complex reducers, it would mean that we have to distinguish between the first invocation and following invocations, since the first accumulator value might be of completely different type than the array elements are (leading to potential undefined is not a function
exceptions during execution, when not carefully handled.
Before you now instantly close this article and start writing reducers that can handle data of the type of array elements AND accumulator type:
Array.prototype.reduce()
provides a way to completely avoid this problem:
It allows you to specify a second parameter, next to your reducer function, to be used as the first accumulator
value. Setting this parameter will completly avoid this problem:
const array = [ 1, 2, 3, 5 ];
const finalResult = array.reduce((accumulator, element) => {
console.log("accumulator: ", accumulator);
console.log("element: ", element);
const result = accumulator + element;
console.log("result: ", result);
return result;
}, 0);
console.log("final result: ", finalResult);
// Console output:
//
// > accumulator: 0
// > element: 1
// > intermediate result: 1
// > accumulator: 1
// > element: 2
// > intermediate result: 3
// > accumulator: 3
// > element: 3
// > intermediate result: 6
// > accumulator: 6
// > element: 5
// > intermediate result: 11
// > final result: 11
//
And it also allows us to pass a different accumulator type (but with same interface), to alter the way data is reduced completely:
const array = [ 1, 2, 3, 5 ];
const finalResult = array.reduce((accumulator, element) => {
console.log("accumulator: ", accumulator);
console.log("element: ", element);
const result = accumulator + element;
console.log("result: ", result);
return result;
}, "ConcatedElements: ");
console.log("final result: ", finalResult);
// Console output:
//
// > accumulator: ConcatedElements:
// > element: 1
// > intermediate result: ConcatedElements: 1
// > accumulator: ConcatedElements: 1
// > element: 2
// > intermediate result: ConcatedElements: 12
// > accumulator: ConcatedElements: 12
// > element: 3
// > intermediate result: ConcatedElements: 123
// > accumulator: ConcatedElements: 123
// > element: 5
// > intermediate result: ConcatedElements: 1235
// > final result: ConcatedElements: 1235
//
Using a string as first accumulator
value, will concat the the elements, instead of adding them.
Available in Different Flavours
Besides Array.prototype.reduce()
there is also Array.prototype.reduceRight()
. This is basically the same thing, but operates into the opposite direction:
const array_left = ['1', '2', '3', '4', '5'];
const array_right = ['1', '2', '3', '4', '5'];
const left = array_left.reduce((accumulator, element) => {
return accumulator + element;
});
const right = array_right.reduceRight((accumulator, element) => {
return accumulator + element;
});
const equivalentRight = array_left.reverse().reduce((accumulator, element) => {
return accumulator + element;
});
const equivalentLeft = array_right.reverse().reduceRight((accumulator, element) => {
return accumulator + element;
});
console.log(left);
console.log(right);
console.log(equivalentRight);
console.log(equivalentLeft);
// Console output:
//
// > "12345"
// > "54321"
// > "54321"
// > "12345"
//
That's it, for this short introduction on array.reduce()
. Maybe you know some handy recipes for reducers (e.g. grouping data, unifying data, transforming an array into an Object (maybe to use it later as hashmap) or any other idea, you are welcome to post it into the comments. I will include the recipes (with a link to the author) in my next article about reducers.
I also appreciate any feedback, critics or corrections.
I hope this article helps to put more Fun into Functional Programming ;-)
Top comments (1)
thanks for writing this up!
Helped me understand the concept of reducers a bit more :)