Use the reduce Method to Analyze Data: 0 and {}?

Tell us what’s happening:
I think that I missed some of the concepts from the previous challenges. Thank you for your help!


Your browser information: Chrome

Challenge: Use the reduce Method to Analyze Data

Link to the challenge:

It’s the initial value of the accumulator. In the sum method, you start at zero. In the second example, it starts with an empty object. It’s also not a proper reducer because it mutates its argument.

2 Likes

Two (well actually more than two) questions…

  1. Why does the second example start with an empty object, as the initial value of the accumulator? What happens to the empty object as reduce iterates over the array, in other words, how does the value of the accumulator change?

    maybe someone can explain the example code below, in pseudocode?

const users = [
  { name: 'John', age: 34 },
  { name: 'Amy', age: 20 },
  { name: 'camperCat', age: 10 }
];

const usersObj = users.reduce((obj, user) => {
  obj[user.name] = user.age;
  return obj;
}, {});
console.log(usersObj); // { John: 34, Amy: 20, camperCat: 10 }
  1. What is a “proper” reducer? In the example code above, which argument is the reducer mutating? Why is not proper for it to mutate its argument? I was not able to find any clarification about this in the MDN documentation below.

Thank you!

Warning, super-long post ahead, because I didn’t have time to make a short one:

The argument being mutated is obj, and it violates the “no WTFs” property that I’m going to explain below. Let’s take the first user John. When obj is passed to the reducer function, it didn’t have a ‘John’ property, but when the reducer is done, it does. The fact that something got changed outside the reducer’s scope means it got mutated. Now this updated value is actually what you want, but a reducer is supposed to return a new value of its accumulator, which means returning a whole new object, not just the same obj mutated. So if we were to fix that, it would look like this.

users.reduce((obj, user) => ({...obj, [user.name]: user.age}), {});

That’s some pretty hairy syntax above, it’s got to do with what the example’s doing, which is something you’d probably never do in real world code. More likely you’d update some existing fixed property on an object or whatnot.

So let’s do that, but we’ll do something a bit simpler and a bit more reflective of real-world code. BTW, throw this code into repl.it or a local file and actually try it out, don’t just follow the text here.

First, let’s do it imperatively, using a loop and state:

const accum = {sum: 0, product: 1}
const nums = [1,2,3,4,5,6,7,8,9]
for (const num of nums) {
   accum.sum += num
   accum.product *= num
}

console.log(accum) //  { sum: 45, product: 362880 }

That code is perfectly understandable, I hope. You have a starting value (accum), and for each of the values (nums) you update that value (by setting the props on accum). Let’s go ahead and turn this into a function.

function sumsAndProducts(nums) {
  accum = {sum: 0, product: 1}
  for (const num of nums) {
    accum.sum += num
    accum.product *= num
  }
  return accum
}

const nums = [1,2,3,4,5,6,7,8,9]
console.log(sumsAndProducts(nums)) //  { sum: 45, product: 362880 }

All well and good, but let’s say you had some results from before and you wanted to pass use another accumulator? No prob, just pass in accum as an argument instead of hardwiring it:

function sumsAndProducts(nums, accum) {
  for (const num of nums) {
    accum.sum += num
    accum.product *= num
  }
  return accum
}

const accum = {sum: 45, product: 362880}
const nums = [10,11,12]
console.log(sumsAndProducts(nums, accum)) //  { sum: 78, product: 479001600 }

console.log(accum) // {sum: 78, product: 479001600}

console.log(sumsAndProducts(nums, accum)) //  { sum: 78, product: 479001600 } // { sum: 111, product: 632282112000 }

console.log(accum) // { sum: 111, product: 632282112000 }

Ruh-roh, we just changed accum. What if we wanted to use that original value for something else? Sure you could require someone to pass in a copy (just {...obj} will do that) but better would be to just not mess with the arguments in the first place, i.e. make it a pure function. There’s lots of reasons to want pure functions, but lack of “WTF, shit changed, I didn’t ask shit to change!” is one of the big ones.

Not a biggie, our function can just return a copy instead:

function sumsAndProducts(nums, accum) {
  const copy = {...accum} // makes a copy
  for (const num of nums) {
    copy.sum += num
    copy.product *= num
  }
  return copy
}

const accum = {sum: 45, product: 362880}
const nums = [10,11,12]
console.log(sumsAndProducts(nums, accum)) // { sum: 78, product: 479001600 }

console.log(accum) // { sum: 45, product: 362880 }

console.log(sumsAndProducts(nums, accum)) // { sum: 78, product: 479001600 }

console.log(accum) // { sum: 45, product: 362880 }

Look at that, nothing actually got changed this time, yay for no WTFs!

Now here’s where start moving toward reduce. Notice how sumsAndProducts is really doing two different things, it’s looping over an array and for each step, it’s doing the summing up operation. Let’s break that last part out into its own function, shall we?

function sumsAndProducts(nums, accum) {
  let current = accum 
  for (const num of nums) {
    current = oneSumAndProduct(current, num)
  }
  return current
}

function oneSumAndProduct(accum, num) {
  return {sum: accum.sum + num, product: accum.product * num}
}

const accum = {sum: 45, product: 362880}

console.log(oneSumAndProduct(accum, 99)) // { sum: 144, product: 35925120 }
console.log(accum) // { sum: 45, product: 362880 }

const nums = [10,11,12]
console.log(sumsAndProducts(nums, accum)) // { sum: 78, product: 479001600 }

console.log(accum) // { sum: 45, product: 362880 }

Notice how oneSumAndProduct doesn’t have any WTFs either, it just returns a new object and doesn’t change stuff from under you. Zero WTFs all around, ain’t that awesome?

Let’s say you wanted to do something else weird with the accumulator like, I dunno, make it sum up only odd values. Or do a running average. Let’s just say it does something and not even care what it is. I’m going to leave out all the console.log stuff from here on out, since I assume you know how to type them by now. You are following along with repl.it or some other js environment, right?

function doSomethingWithNums(nums, accum) {
  let current = accum 
  for (const num of nums) {
    current = doSomethingWith(current, num)
  }
  return current
}

function doSomethingWith(accum, num) {
  return {oddSums: (accum.oddSums + ((num % 2) ? num : 0))}
}

Look at that loop, it’s exactly the same as before, but just a different function name, just takes the same args even. So let’s make it so we pass in the function instead.

function runFunctionOverNums(nums, func, accum) {
  let current = accum 
  for (const num of nums) {
    current = func(current, num)
  }
  return current
}

function sumIfOdd(accum, num) {
  return {sum: accum.sum + ((num % 2) ? num : 0) }
}

function sumIfEven(accum, num) {
  return {sum: accum.sum + ((num % 2) ? 0 : num) }
}


// I lied about console.log, but you can check the output for yourself
console.log(runFunctionOverNums([1,2,3,4,5], sumIfOdd, {sum: 0}))

Notice how runFunctionOverNums has become totally generic. It doesn’t even really need an object really. What if we wanted just a sum without even using an object: No problem.

function runFunctionOverNums(nums, func, accum) {
  let current = accum 
  for (const num of nums) {
    current = func(current, num)
  }
  return current
}

function sum(accum, num) {
   return accum + num
}

console.log(runFunctionOverNums([1,2,3,4,5], sum, 0))

Let’s give things suitably generic names:

function reduce(arr, func, accum) {
  let current = accum 
  for (const item of arr) {
    current = func(current, item)
  }
  return current
}

reduce([1,2,3,4,5], (a, b) => a + b, 0)

Now let’s make it a method on Array (for demo purposes, don’t mess with Array.prototype in real life)

Array.prototype.myReduce = function (func, accum) {
  let current = accum 
  for (const num of this) {
    current = func(current, num)
  }
  return current
}

console.log([1,2,3,4,5].myReduce((a, b) => a + b, 0))
console.log([1,2,3,4,5].reduce((a, b) => a + b, 0))


We just wrote Array.reduce.


Addendum: functional programmers refer to the “No WTFs” proroperty as “referential integrity”, or “equational reasoning”. Cuz that’s totally understandable to non-mathematicians, right?

1 Like

@chuckadams, thanks for the detailed explanation on mutation. I think I understand your examples.

I’m still having trouble understanding the example code in the exercise:

const users = [
  { name: 'John', age: 34 },
  { name: 'Amy', age: 20 },
  { name: 'camperCat', age: 10 }
];

const usersObj = users.reduce((obj, user) => {
  obj[user.name] = user.age;
  return obj;
}, {});
console.log(usersObj); // { John: 34, Amy: 20, camperCat: 10 }

Here is my understanding:

obj is the accumulator and the initial value of obj the accumulator is set to an empty object. So for the first iteration of reduce, the accumulator obj = { } and the current value user is the first value in the array, which is the object { name: 'John', age: 34 }.

However, I don’t understand what is happening in the callback function body:

{
  obj[user.name] = user.age;
  return obj;
}

This is my understanding, which is really not understanding, since it doesn’t give the expected output:

If obj = {}, then obj[user.name] has no value, because it’s an empty object, right?
Then we return the empty object, and we iterate over the next value. But the accumulator is still an empty object, right? Which means at the next iteration we again start with an empty object. This is where I’m stuck right now.

@Sharad823 Below line actually create a new property inside obj called user.name (actually the value of user.name in that instance) and assign its value to user.age. Remember this is just assignment operator. It is not checking whether there exists this property inside obj. Its just adding dynamically named props to obj object

obj[user.name] = user.age;

Try below code as well and you should understand the role of accumulator. Here the empty object {} or in this case {Jane:40}

const users = [
{ name: ‘John’, age: 34 },
{ name: ‘Amy’, age: 20 },
{ name: ‘camperCat’, age: 10 }
];

const usersObj = users.reduce((obj, user) => {
obj[user.name] = user.age;
return obj;
}, {Jane:40});
console.log(usersObj);

@acskck, thanks for the explanation.

Given the long explanation of reducers and how they should stay pure functions, we can go back to the reducer in the sample code, namely this, and see where it’s not ideal:

users.reduce((obj, user) => {
  obj[user.name] = user.age;
  return obj;
}, {});

Here, it’s mutating and returning obj, which a reusable reducer function is never supposed to do. However, it’s actually not all that bad here in this “one-liner”, since obj never actually leaves the scope of the reduce call. There’s no outside data that would get passed in and manipulated behind your back, so no real WTF violations. It’s mutating state, but it’s all local state. It does however fall down on the WTF factor when you want to make either part reusable, either the reducer function, or the initial value, like we did with the accumulator in my example.

Moral of the story: when you’re doing functional programming, it pays to get into the habit of keeping every function pure, whether you need to or not.

1 Like