loading...

Array reduce

jamesrweb profile image James Robb Updated on ・4 min read

A reducer is a function that takes a collection and for each item in the collection, returns a new state. Most commonly we can use reducers to transform an old state of something to a new state of something. That could be an array to integer, array to array, array of objects representing application state to a new array of objects with the updated application state, anything really.

In most implementations the reduce function relies on 3 key components being available. Firstly is the collection to be reduced, secondly is the reducer function to run for each item in the collection and thirdly is the initial value of the reducer. As an example, in vanilla JavaScript we could do the following:

const numbersToAdd = [1, 2, 3];

function additionReducer(previous, current) {
  return previous + current;
}

const result = numbersToAdd.reduce(additionReducer, 0);
console.log(result); // 6

We reduce our collection passing in a reducer function which receives a previous and current value and adds the two together and finally we have the initial value of 0. What this will do is run the reducer for each iteration of the collection and use the initial value as the initial value of previous and when we return the result of adding previous and current, that value will then become the value of previous on the next iteration until there are no more items in the collection to iterate and thus, the result is returned.

Tests

describe('reduce', () => {
  it('should apply the addition reducer correctly', () => {
    const collection = [1, 2, 3];
    const reducerFn = (previous, current) => previous + current;
    const actual = reduce(collection, reducerFn, 0);
    const result = 6;
    expect(actual).toStrictEqual(result);
  });

  it('should return a new array of multiplied values correctly', () => {
    const collection = [1, 2, 3];
    const reducerFn = (previous, current) => {
      previous.push(current * 2);
      return previous;
    };
    const actual = reduce(collection, reducerFn, []);
    const result = [2, 4, 6];
    expect(actual).toStrictEqual(result);
  });

  it('should reduce a collection of objects and reshape them via the reducer', () => {
    const pokemon = [{
        name: "charmander",
        type: "fire"
      },
      {
        name: "squirtle",
        type: "water"
      },
      {
        name: "bulbasaur",
        type: "grass"
      }
    ];

    function pokemonReducer(output, current) {
      output[current.name] = {
        type: current.type
      };
      return output;
    }

    const actual = reduce(pokemon, pokemonReducer, {});
    const result = {
      charmander: {
        type: 'fire'
      },
      squirtle: {
        type: 'water'
      },
      bulbasaur: {
        type: 'grass'
      }
    };
    expect(actual).toStrictEqual(result);
  });
});

Here we can see 3 reduce tests which work on similar data but produce values of differing types. That is to say that we have a simple addition reducer just as with the example given in the introduction of this article but also a more complex multiplication reducer which basically acts like a map function would since it generates a new array of multiplied values. Lastly we see a far more complex reducer which takes a collection of objects and returns a new state representation of each object as a new collection.

Implementation

The native JavaScript implementation of reduce has the following signature:

arr.reduce(function callback(accumulator, currentValue[, index[, array]]) {
  // perform actions and return the next state
}[, initialValue]);

We will aim to reproduce this behaviour with the following implementation:

/**
 * @function reduce
 * @description A function to a collections values into any other type
 * @param {Array} collection - The collection to reduce
 * @param {Function} reducerFn - The reducer function to be applied on the last and current value
 * @param {*} initialValue - The initial value to apply the reducer to
 * @returns {*} The reduced value, this will be the same type as the initialValue parameter
 */
function reduce(collection, reducerFn, initialValue) {
  let output = initialValue;
  const clone = [...collection];

  for (let index = 0; index < clone.length; index++) {
    output = reducerFn(output, clone[index], index, clone);
  }

  return output;
}

The initialValue will be the default output of the reduce function if no items in the collection exist. If items exist in the collection then for each one we will reassign output to the value of the reducerFn function. The reducerFn function takes the same parameters as the native JavaScript implementation since that is our goal to reproduce. These parameters are the accumulator, currentValue, index, array in the native implementation but in our case they are output, clone[index], index and clone.

Note: If you haven't read the previous articles on Array method algorithms in this series, we are cloning the array to avoid mutations on the initial collection that is provided so that that remains intact.

Finally, once our reducerFn function commits actions against each element and generates a final output value, we exit the loop and return the output value.

Using our example of the native implementation near the top of this article, we could do the following to achieve the same results:

const numbersToAdd = [1, 2, 3];

function reduce(collection, reducerFn, initialValue) {
  let output = initialValue;
  const clone = [...collection];

  for (let index = 0; index < clone.length; index++) {
    output = reducerFn(output, clone[index], index, clone);
  }

  return output;
}

function additionReducer(previous, current) {
  return previous + current;
}

const result = reduce(numbersToAdd, additionReducer, 0);
console.log(result); // 6

Conclusions

Reducers can be quite a complex topic to discuss but just remember, a reducer merely reduces a collection to a single value. That value could be anything you want it to be but that is all it does. I love using reducers in my day to day work as they can make complex tasks much easier and libraries such as Redux use reducers as a core part of their functionality to do some real heavy lifting. Reducers are also useful for mundane tasks though such as our additionReducer example and so you can adapt them to many use cases quite easily. In saying this though you do want to scope reducers to highly specific use cases and they should adhere strictly to the Single Responsibility Principle as with any function or method implementation.

Posted on by:

jamesrweb profile

James Robb

@jamesrweb

I am a Polyglot Software Engineer focussed on the web πŸ§‘πŸ»β€πŸ’», an ardent Accessibility Advocate β™Ώ, amateur creative coder πŸ§‘πŸ»β€πŸŽ¨ and an aspiring teacher πŸ‘¨πŸ»β€πŸ«

Discussion

markdown guide