Reducers are hard

But they don't have to be

Reducers are hard

Photo by Sigmund on Unsplash

When learning a programming language, some concepts might be hard. Modern languages like Java, C# and JavaScript all have a way of reducing collections. In Java, all collections can be streamed and then reduced. In C# this is done by using the Aggregate method. In JavaScript, arrays also have a reduce function for a while now.

In this blog post I aim to explain what it means to be reducing, how reducing can be done and how it connects to the redux algorithm that is so popular in frontends. I will be using JavaScript in my examples, but the concept stays the same in all the languages.

What is reducing?

While I'm going to stick to JavaScript for the examples in this blog post, C# put it best. Reducing is nothing more than aggregating a collection. Some languages allow us multiple way of doing this with different parameters that can be passed to their reduce methods. In general, the idea is this:

  • you indicate how to start your aggregation;
  • you describe the aggregation function.

It's written like this in the JavaScript documentation:

/**
 * Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.
  * @param callbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.
  * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.
   */
reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U;

Let's have a look at how it works behind the scenes to better understand what this function is doing.

// Assume we have a variable called 'array'
let aggregation: U = initialValue;

for(const currentIndex = 0; currentIndex < array.length; currentIndex++) {
  const currentValue = array[currentIndex];
  const previousValue = aggregation;
  aggregation = callbackfn(previousValue, currentValue);
}

return aggregation;

To give you full flexibility on how to implement the callbackfn, JavaScript also provides you the currentIndex and the array along with the previousValue of the aggregation and the currentValue of the collection. This was omitted in the example above.

Your software will loop over your collection and aggregate all elements into something new. Although it can happen, your result might not have the same type as your current collection. Most programming languages support this and in the next examples we will have a look at how this works.

Example 1: Sum

A reducer is perfect for calculating the sum of a collection of numbers. You have a starting value - the number 0 - and you aggregate your elements by adding them. In JavaScript, it could look like this:

function sum(array: number[]) {
  return array.reduce((res, value) => res + value, 0);
}

Let's break this example down to the for loop again to see what we're actually doing.

function sum(array: number[]) {
  let aggregation = 0;

  for(const currentIndex = 0; currentIndex < array.length; currentIndex++) {
    const currentValue = array[currentIndex];
    const previousValue = aggregation;
    aggregation = previousValue + currentValue;
  }

  return aggregation;
}

When we refactor that and make it more readable, it looks like this:

function sum(array: number[]) {
  let sum = 0;

  for(const index = 0; index < array.length; index++) {
    sum = sum + array[index];
  }

  return sum;
}

Example 2: A groupBy function

To understand the next example, you need to understand some JavaScript features like the spread syntax, the conditional operator and property accessors. An object property can be accessed via obj.property or via obj['property'].

function groupBy<T>(values: T[], field: string): { [key: string]: T[] } {
  return values.reduce(
    (res, t) => ({
      ...res, [t[field]]: res[t[field]] ? [...res[t[field]], t] : [t],
    }),
    {},
  );
}

That's a whole bunch of unreadable code. But what does it do? To understand that, let's rewrite it as a for loop again.

function groupBy<T>(values: T[], field: string): { [key: string]: T[] } {
  let aggregation = {};

  for(const currentIndex = 0; currentIndex < values.length; currentIndex++) {
    const currentValue = array[currentIndex];
    const previousValue = aggregation;

    const fieldValue = currentValue[field];

    if(previousValue[fieldValue] !== undefined) {
      aggregation = {
        ...previousValue,
        [currentValue[field]]: [currentValue],
      };
    } else {
      aggregation = {
        ...previousValue,
        [fieldValue]: [...previousValue[fieldValue], currentValue],
      };
    }
  }

  return aggregation;
}

This algorithm groups elements of the array values by the value of a field they have in common. The code below should explain:

const values = [
  { id: 1, color: 'RED' },
  { id: 2, color: 'BLUE' },
  { id: 3, color: 'GREEN' },
  { id: 4, color: 'RED' },
];

const result = groupBy(values, 'color');

console.log(result);
/* prints the following
{
  RED: [{ id: 1, color: 'RED' }, { id: 4, color: 'RED' }],
  BLUE: [{ id: 2, color: 'BLUE' }],
  GREEN: [{ id: 3, color: 'GREEN' }],
}
*/

The objects are grouped by the color field. Please note that in JavaScript, property order doesn't matter so the result object might have the ordering of its colours changed. The grouped objects however are always the same for each color.

Example 3: Implementing a filter

This example is meant to show you how to filter a collection through its reduce function. It is not meant to tell you this is how you should filter your collections. Most programming languages have filter methods or functions available. Use those!

function filter(array: number[], filterFn: (number) => boolean) {
  return array.reduce((previousValue, currentValue) => {
    if(filterFn(currentValue)) {
      return [...previousValue, currentValue];
    }

    return previousValue;
  }, []);
}

You don't need to use all the values of the collection when reducing. In the aggregator function you can use if statements or manipulate values.

If the reducer above is still difficult to understand, can you transform it into a for loop like we did previously? Does that help?

The code below shows how our filter function could be used.

const values = [1, 2, 3, 4, 5, 6];
const result = filter(values, num => num % 2 === 0);

console.log(result);
/* prints the following
[2, 4, 6]
*/

How this relates to Redux

The redux algorithm is explained in detail in many blog posts on the internet. Allow me to sum it up real quickly.

Define Actions that can be dispatched throughout your application. Alter your application State by implementing a reducer function. Read parts of the application state by Selecting what you want.

Here's an example of a redux reducer in its simplest form.

function myReducer(currentState, action) {
  if (action.type === 'myType') {
    return changeStateForTheBetter(currentState);
  }

  return currentState;
}

In the beginning of this blog post I noted how C# gets the name of the reducer function right. It's an aggregator function and so is the reducer function of the redux algorithm. What it translates to is the following:

Given the current state and an action, we can change the state when certain actions occur.

Or you might read the reducer function like this:

Given a set of actions that we care about. When one of those actions occurs, change the state by changing its properties. If we're not interested in the action that occurs, we don't alter the state at all.

When reducing collections, our aggregator function describes how the aggregation is done. The same goes for the redux reducer function. It describes how the state (the aggregation of application data) should change based on an action, which is the current value that passes through the action list of the application. We aggregate our state by altering, adding or removing properties from it.

Conclusion

Reduce functions can be hard to understand, especially if you don't fully understand what the reduce function itself is doing. An aggregator function is used when looping over the elements of your collection. Through examples, we looked at how the reducing collections can be translated to old-school for loops.

Based on this mechanism, the redux algorithm uses reducer functions to aggregate your application state. I think it's a missed opportunity that the dev community has named the function to change your state a reducer. Maybe if the function was called aggregator or something like that, learning redux might become a bit easier.