user11092881
user11092881

Reputation:

Accumulator function inside reduce is wrapped by a function in Javascript using closure

var arr = [{ name: "John", score: "8.8" }, { name: "John", score: "8.6" }, { name: "John", score: "9.0" }, { name: "John", score: "8.3" }, { name: "Tom", score: "7.9" }],
    avgScore = arr.reduce(function (sum, count) {
        return function (avg, person) {
            if (person.name === "John") {
                sum += +person.score;
                return sum / ++count;
            }
            return avg;
        };
    }(0, 0), 0);

console.log(avgScore);

I found this interesting code and I was wondering how closure exactly worked in Javascript. I was taken aback by the fact that the function with the accumulator and the iterated element is wrapped by another function. Isn't reduce supposed to accept a function with the the accumulator and the iterated element, then how come the reduce function still works despite the fact that the accumulator function with the iterated element with avg is wrapped by another function?

Also, how come we call the function with a closure using (0,0), but in the second iteration we call it with the updated sum and count (sum, 1). Shouldn't a closure use the arguments (0, 0) over and over again?

Upvotes: 0

Views: 196

Answers (2)

KooiInc
KooiInc

Reputation: 122906

Seems like a bit of a complex way to determine the average score of John. It uses the closure to be able to conditionally increment the value of count. I've tried to explain it using logs of the running values.

Anyway the average determination could be hugely simplified (see second part of the snippet)

const arr = [
  { name: "John", score: "8.8" },
  { name: "John", score: "8.6" },
  { name: "John", score: "9.0" },
  { name: "John", score: "8.3" },
  { name: "Tom", score: "7.9" } ];

// the outer function delivers the inner function
// the outer function uses 0, 0 as input initially
// The outer function does nothing with those values
// and it returns not a value, but the inner function.
// The inner function is run every iteration of reduce
// and in there the initial closed over values are 
// manipulated. That inner function returns the actual
// accumulator value (a running average)
const avgScoreDemo = arr.reduce(function(sum, count) {
  let outerValues = `outer sum: ${sum}, outer count: ${count}`;
  return function(avg, person) {
    console.log(outerValues +
      ` avg: ${avg}, inner sum ${sum}, inner count: ${count}`);
    if (person.name === "John") {
      sum += +person.score;
      return sum / ++count;
    }
    return avg;
  };
}(0, 0), 0);

// This can be simplified to
const avgScore = arr.reduce((sum, person) =>
    sum + (person.name === "John" ? +person.score : 0), 0) /
  arr.filter(v => v.name === "John").length;
console.log(avgScore);

// or using filter/index value
const avgScore2 = arr
  .filter( v => v.name === "John" )
  .reduce( (acc, person, i) =>
    ({ ...acc, sum: acc.sum + +person.score, 
        average: (acc.sum + +person.score) / ++i }), 
    { average: 0, sum: 0 } ).average;
console.log(avgScore2);
.as-console-wrapper {
  top: 0;
  max-height: 100% !important;
}

Upvotes: 0

mtkopone
mtkopone

Reputation: 6443

That is a very obfuscated way to use reduce. But it does work, however.

The function (sum, count) is instantly invoked with (0,0), returning the function (avg, person), which is then used by the reduce for each element, starting with the accumulator 0, and returning a new average for every iteration even though only the last value is actually used. It works by updating the sum and count variables within the closure for every iteration.

A more readable way to calculate the average using reduce would be:

const result = arr.reduce(function (acc, person) {
  if (person.name === "John") {
    return {
        sum: acc.sum + parseFloat(person.score),
        count: acc.count + 1
    }
  }
  return acc
}, { sum: 0, count: 0 })
console.log(result.sum / result.count)

But since the point is to just calculate the average score of a person, an even more readable and even shorter way would be:

const johnsScores = arr.filter(person => person.name === 'John')
const total = johnsScores.reduce((acc, person) => acc + parseFloat(person.score), 0)
console.log(total / johnsScores.length)

Upvotes: 0

Related Questions