DEV Community

Cover image for Array.reduce
Tracy Gilmore
Tracy Gilmore

Posted on • Edited on

Array.reduce

Why is the sea salty? Simple question but stay with me because I think the water cycle is a good analogy for the way reduce method really works.

In short, water (H2O) evaporates from the surface of the oceans to form clouds. The clouds pass over land and condense until they start to precipitate as rain/snow fall. Eventually the falling water runs into rivers and starts on it way to the sea. En route the water picks up minerals including salts and carries them to the sea. As the cycle starts again the minerals are left behind and over time concentration levels build.

To see how the water cycle can help us understand the way reduce works we have to break it down into three element:

  • The minerals equate to the items in the array on which we are performing the reduce.
  • The water is the accumulator parameter or the reducer callback function.
  • The ocean is the accumulator in its argument form, both in the initial and final values.

So let's align this to code

Please excuse the concentration measurements, they are probably way off, I am not a chemist.

First we will prepare some test data. The following code generates simulated samples, two per month for a years.

const mineralsPerCycle = concentrationSamplesOverYear(24);
console.table(mineralsPerCycle);

function concentrationSamplesOverYear(samples) {
  const interval = (2 * Math.PI) / samples;
  const captureSample = i => 
    ((Math.random() + 7) / 8) * ((Math.cos(i * interval) + 2) / 3);
  return [...new Array(samples)].map((_, i) => captureSample(i));
}
Enter fullscreen mode Exit fullscreen mode

The console.table will render the values before we use them. Below is an example but yours will have different values.

┌─────────┬─────────────────────┐
│ (index) │       Values        │
├─────────┼─────────────────────┤
│    0    │  0.89801916280756   │
│    1    │ 0.9567662790947499  │
│    2    │ 0.9325939089002321  │
│    3    │ 0.8992754278881672  │
│    4    │ 0.7532231143389726  │
│    5    │ 0.6765845269058688  │
│    6    │ 0.6187743088061717  │
│    7    │ 0.5157538308846997  │
│    8    │ 0.46555646525988514 │
│    9    │ 0.38054565223528175 │
│   10    │ 0.33107496732400704 │
│   11    │ 0.3348125096349211  │
│   12    │ 0.30271050596599436 │
│   13    │ 0.30352471441053985 │
│   14    │ 0.3696661578004031  │
│   15    │ 0.4156042590776569  │
│   16    │ 0.4608111994637522  │
│   17    │  0.53172225574472   │
│   18    │ 0.6594949154650602  │
│   19    │ 0.6714790771824638  │
│   20    │ 0.7728233018044018  │
│   21    │ 0.8208884212567936  │
│   22    │  0.924437922104001  │
│   23    │ 0.9497900622814304  │
└─────────┴─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Next we will simulate the accumulation of minerals as implied by each fortnightly sample.

let oceanConcentration = 0;
console.log(`
Initial concentration = ${oceanConcentration} mgs/ltr
`);

oceanConcentration = mineralsPerCycle.reduce(
  waterCycle,
  oceanConcentration);

console.log(`
Final concentration = ${oceanConcentration} mgs/ltr
`);

function waterCycle(currentConcentration, cycleConcentration) {
  return currentConcentration + cycleConcentration;
}

/* Output

Initial concentration = 0 mgs/ltr

Final concentration = 14.945932946637733 mgs/ltr

*/
Enter fullscreen mode Exit fullscreen mode

Note in the above code how we have simplified the calling of the reduce method by firstly referencing the callback function and secondly referencing a variable for the initial value of the accumulator.

function waterCycle(currentConcentration, cycleConcentration) {
  const newConcentration = currentConcentration + 
    cycleConcentration;
  console.log(`${cycleConcentration} + ${
    currentConcentration} = ${
    newConcentration}`);
  return newConcentration;
}
Enter fullscreen mode Exit fullscreen mode

If we replace the waterCycle reducer for the above version we can see the concentration 'accumulate' with each sample.

0.89801916280756 + 0 = 0.89801916280756
0.9567662790947499 + 0.89801916280756 = 1.85478544190231
0.9325939089002321 + 1.85478544190231 = 2.787379350802542
0.8992754278881672 + 2.787379350802542 = 3.686654778690709
0.7532231143389726 + 3.686654778690709 = 4.439877893029681
0.6765845269058688 + 4.439877893029681 = 5.11646241993555
0.6187743088061717 + 5.11646241993555 = 5.735236728741722
0.5157538308846997 + 5.735236728741722 = 6.2509905596264215
0.46555646525988514 + 6.2509905596264215 = 6.716547024886307
0.38054565223528175 + 6.716547024886307 = 7.097092677121588
0.33107496732400704 + 7.097092677121588 = 7.428167644445595
0.3348125096349211 + 7.428167644445595 = 7.762980154080516
0.30271050596599436 + 7.762980154080516 = 8.06569066004651
0.30352471441053985 + 8.06569066004651 = 8.369215374457049
0.3696661578004031 + 8.369215374457049 = 8.738881532257452
0.4156042590776569 + 8.738881532257452 = 9.154485791335109
0.4608111994637522 + 9.154485791335109 = 9.61529699079886
0.53172225574472 + 9.61529699079886 = 10.14701924654358
0.6594949154650602 + 10.14701924654358 = 10.806514162008641
0.6714790771824638 + 10.806514162008641 = 11.477993239191106
0.7728233018044018 + 11.477993239191106 = 12.250816540995508
0.8208884212567936 + 12.250816540995508 = 13.071704962252301
0.924437922104001 + 13.071704962252301 = 13.996142884356303
0.9497900622814304 + 13.996142884356303 = 14.945932946637733
Enter fullscreen mode Exit fullscreen mode

Unsurprisingly the callback function of the reduce method (parameter one) is called a reducer. However, one thing that confuses matters is that the callback is not called a reducer because it 'reduces' an array of (potentially) many items into a single value (it might not). It is called a reducer because (for each element of the array) it takes two arguments (primarily, we will expand on this point later) the accumulator and the element. It then reduces them to a single value to form the new accumulator.

On the point of how many parameters the Array.reduce method expects, it actually expects up to four:

  1. The accumulator - the in-bound reduced value
  2. The item - the element from the array to be reduced
  3. The index of the element of the array (not often used)
  4. The array being processed (not reduced), very rarely used.

We will explore the fact the output might not be a single value in the next section.

Reduce, the root of many methods

The reduce method is capable of many operations (we will explore this later) and once mastered it is easy to find opportunities to use it but there are usually better options.

The map method

Like reduce the map method takes a callback but in this case it is a mapping function that takes a value from the array and produces a new value, one for one. The new array that is created will be the same size as the input array.

If we use map in the following fashion,

function celsiusToFahrenheit(degCelsius) {
   return (degCelsius * 9) / 5 + 32;
}

console.table([-40, 0, 16, 100].map(celsiusToFahrenheit));
Enter fullscreen mode Exit fullscreen mode

a table of temperatures in Fahrenheit will be presented on the console for each of the Celsius temperatures in the input array.
This can also be written using the reduce method as follows using the same mapping function.

console.table([-40, 0, 16, 100].reduce((acc, celsius) =>
   [...acc, celsiusToFahrenheit(celsius)], []));
Enter fullscreen mode Exit fullscreen mode

The filter method

We can do something similar to reproduce the filter method using a predicate function such as:

const greaterThanFifty = (value) => value > 50;

console.table([20, 40, 60, 80, 100].filter(greaterThanFifty));
// 60, 80, 100
Enter fullscreen mode Exit fullscreen mode

Now with the reduce method.

console.table([20, 40, 60, 80, 100].reduce((acc, val) =>
   greaterThanFifty(val) ? [...acc, val] : acc, [])); 
Enter fullscreen mode Exit fullscreen mode

In both examples using reduce make for a longer and slightly more complicated solution. However, reduce can combine both operations in a single pass.

console.table(
    [-40, 0, 16, 100].reduce((acc, celsius) => {
        const fahrenheit = celsiusToFahrenheit(celsius);
        return greaterThanFifty(fahrenheit) ? 
            [...acc, fahrenheit] : acc;
    }, [])
); // [60.8, 212]
Enter fullscreen mode Exit fullscreen mode

In fact the output of a reduce does not even have to be an array.

console.table(
    [-40, 0, 16, 100].reduce(
        (acc, celsius) => ({ ...acc, [celsius]: 
            celsiusToFahrenheit(celsius) }),
        {}
    )
); // {'16': 60.8, '100': 212}
Enter fullscreen mode Exit fullscreen mode

The above example will produce an object containing a mapping of the Celsius to Fahrenheit temperatures but only for those Fahrenheit temperatures greater than 50 degrees.

Do's and Don'ts

In this final section I would like to offer some advice for using the reduce method.

Do's

  1. Use reduce when converting from an array to another data structure.
  2. Consider using the reduce method when the operation is a combination of mapping and filtering.

Don'ts

  1. Do not use reduce when there are better alternative methods. They well usually perform better as they are implemented within the JavaScript engine.
  2. Don't be scared to at least explore using the reduce method when appropriate.

Reduce's evil twin

The reduce method is not likely to be a method you use every day but knowing it exists and what is capable of adds another tool to your toolbox.

An even less used array method in reduce's (not so) evil twin reduceRight, which I think is fairly obvious what it does. Reduce processes the items in the array from left to right (in index order), reduceRight processes the array from right to left (in inverse index order). But reduceRight is not equivalent to Array.reverse().reduce() because the third parameter of the reducer function will decrease not increase as the method traverses the array.

Top comments (0)