Pure Reduce Functions - Use Aggregator As A State
Published: 2022-05-09 13:14:09

when using `Array.reduce()` for complex operations, we tend to resort to using side effects (e.g. defining a `let` variable or an object out of scope. and modify them while iterating).

Here's an easy way to keep your reduce functions pure by using the aggregator as a state.

---

Consider a reduce function that's meant to sum all numbers, and add the largest number twice.
We can declare a mutable variable before, use it to track the biggest number, then add it after we're done reducing:
```javascript
let biggestNum = Number.NEGATIVE_INFINITY;

const sum = arr.reduce(
  (agg, val) => {
    biggestNum = Math.max(val, biggestNum);
    return agg + val;
  },
  0,
);

const sumAndBiggest = sum + biggestNum;
```

Alternatively, we can encapsulate all related logic INSIDE the reduce function:

```javascript
const sumAndBiggest = arr.reduce(
  (agg, val, index, sourceArr) => {
    if (index === sourceArr.length - 1) {
      return agg.sum + val + agg.biggestNum;
    }

    return {
      sum: agg.sum + val,
      biggestNum: Math.max(val, agg.biggestNum),
    };
  },
  { sum: 0, biggestNum: Number.NEGATIVE_INFINITY },
);
```

You'll notice two things:
- We use the aggregator as a state - we include the actual sum we're reducing, and the biggest number we found so far.
- We use the 3rd and 4th arguments of the reduce function - the current index and the reference to the array we're iterating over - to see if the current iteration is the last. If it is, we add the largest number and return the final sum instead of a new state.

---

This way adds a bit more logic to the reduce function. Why should we use it then?
Because it allows us to encapsulate all of the data needed for the reduction operation. This means:

- You don't get leftover variables after the reduction is done. When someone reads the code or debugs it, they won't have to worry about those variables - `biggestNum` in our first example - are being used elsewhere.
- Your reduce function is a pure function. Everything you need to look at happens inside the function and its arguments. This also means easier debugging.
- All of the state's data is garbage-collected when it's no longer used.

---

Unfortunately, TypeScript only offers an option to define one return value for `reduce`, and doesn't allow us to differentiate between the final and intermediary return values.
This means we'll have to define the reduce function as being able to return both types, and use the `as` keyword to assert our aggregator's type before use:
```typescript
interface IReduceState {
  sum: number,
  biggestNum: number;
}
const sumAndBiggest = arr.reduce<number | IReduceState>(
  (agg, val, index, sourceArr) => {
    const { sum, biggestNum } = agg as IReduceState
    if (index === sourceArr.length - 1) {
      return sum + val + biggestNum;
    }

    return {
      sum: sum + val,
      biggestNum: Math.max(val, biggestNum),
    };
  },
  { sum: 0, biggestNum: Number.NEGATIVE_INFINITY },
);
```
If you have a nicer solution to this, please let me know!

Happy coding!

---

_Thank you, [Yonatan Kra](https://yonatankra.com/), for your kind review._