The Array.reduce Method Part 5 - Making a Compose Operation

Published: Wednesday, June 1, 2022

Greetings, friends! This is Part 5 of my tutorial series on the Array.reduce method in JavaScript! In this tutorial, I will discuss how to create your very own compose function. A compose function is very similar to the pipe function we discussed in Part 3, but you specifiy operations in reverse.

Function Composition

In Part 3, I briefly discussed function composition. In this tutorial, I'd like to revisit that topic and discuss function composition in more detail.

Function composition is commonly used in mathematics.

text
Copied! ⭐️
f(x) = function of some value, x
g(x) = a different function of some value, x

h(x) = g(f(x)) = function "g" composed with function "f"

In JavaScript, we can use function composition to create a new function:

javascript
Copied! ⭐️
function f(x) {
  return x * 2;
}

function g(x) {
  return x * 3;
}

function h(x) {
  return g(f(x));
}

// h(5) = (5 * 2) * 3
console.log(h(5)); // 30

Using arrow functions, we can make the code significantly shorter:

javascript
Copied! ⭐️
const f = x => x * 2;
const g = x => x * 3;
const h = x => g(f(x));

// h(5) = (5 * 2) * 3
console.log(h(5)); // 30

This implementation of function composition works, but it could be cleaner. Imagine if we had to perform a lot of function compositions. Our code would start becoming harder to read.

javascript
Copied! ⭐️
const array = [-1, 1, -2, 2, -3, 3, -4, 4];

const mapDouble = array => array.map(x => x * 2);
const mapTriple = array => array.map(x => x * 3);
const filterEven = array => array.filter(x => x % 2 === 0);
const filterPositive = array => array.filter(x => x > 0);

const result = mapTriple(filterEven(filterPositive(mapDouble(array))));

console.log(result); // [6, 12, 18, 24]

It would be nicer if we had a compose function that would accept a list of functions as arguments.

Compose Operation Using Array.reduceRight

In Part 4, we learned that the Array.reduceRight method exists. We can use it to create a compose function:

javascript
Copied! ⭐️
const compose = (...functions) => initialValue => functions.reduceRight(
  (acc, fn) => fn(acc),
  initialValue
)

You may notice that this is very similar to the pipe function we created in Part 3.

javascript
Copied! ⭐️
const pipe = (...functions) => initialValue => functions.reduce(
  (acc, fn) => fn(acc),
  initialValue
)

Yep! That's right. The compose function is implemented the same way as the pipe function, but it uses the reduceRight method instead to iterate through an array in reverse.

Let's use our shiny new compose function in an example.

javascript
Copied! ⭐️
const compose = (...functions) => initialValue => functions.reduceRight(
  (acc, fn) => fn(acc),
  initialValue
)

const array = [-1, 1, -2, 2, -3, 3, -4, 4];

const mapDouble = array => array.map(x => x * 2);
const mapTriple = array => array.map(x => x * 3);
const filterEven = array => array.filter(x => x % 2 === 0);
const filterPositive = array => array.filter(x => x > 0);

// Similar to: mapTriple(filterEven(filterPositive(mapDouble(array))))
const compositeFunction = compose(
  mapTriple,
  filterEven,
  filterPositive,
  mapDouble
);

const result = compositeFunction(array);

console.log(result); // [6, 12, 18, 24]

Our code looks much nicer now! Notice that the compose function will return a new function. If we wanted to run the new "composed" function directly on a value, we could rewrite the code to the following:

javascript
Copied! ⭐️
// Same as: mapTriple(filterEven(filterPositive(mapDouble(array))))
const result = compose(
  mapTriple,
  filterEven,
  filterPositive,
  mapDouble
)(array);

console.log(result); // [6, 12, 18, 24]

The following table shows what happens during each iteration of the reduceRight method inside the compose function.

iterationaccumulatorcurrent valuereturn value
1st-1, 1, -2, 2, -3, 3, -4, 4mapDouble-2, 2, -4, 4, -6, 6, -8, 8
2nd-2, 2, -4, 4, -6, 6, -8, 8filterPositive2, 4, 6, 8
3rd2, 4, 6, 8filterEven2, 4, 6, 8
4th2, 4, 6, 8mapTriple6, 12, 18, 24

Compose vs Pipe

The main difference between compose and pipe is that the operations supplied to the function argument of pipe should be in reverse to achieve the same result as compose.

Let's look at an example.

javascript
Copied! ⭐️
const compose = (...functions) => initialValue => functions.reduceRight(
  (acc, fn) => fn(acc),
  initialValue
)

const pipe = (...functions) => initialValue => functions.reduce(
  (acc, fn) => fn(acc),
  initialValue
)

const array = [-1, 1, -2, 2, -3, 3, -4, 4];

const mapDouble = array => array.map(x => x * 2);
const filterEven = array => array.filter(x => x % 2 === 0);

const composedResult = compose(mapDouble, filterEven)(array);
const pipedResult = pipe(filterEven, mapDouble)(array);

console.log('Composed result: ', composedResult); // [-4, 4, -8, 8]
console.log('Piped result: ', pipedResult); // [-4, 4, -8, 8]

The following table shows what happens during each iteration of the reducer inside both the compose and pipe functions in the above example.

iterationaccumulatorcurrent valuereturn value
1st-1, 1, -2, 2, -3, 3, -4, 4filterEven-2, 2, -4, 4
2nd-2, 2, -4, 4mapDouble-4, 4, -8, 8

The order of operation definitely matters. Let's see what happens if we passed operations to the pipe function in the same order as the compose function.

javascript
Copied! ⭐️
const compose = (...functions) => initialValue => functions.reduceRight(
  (acc, fn) => fn(acc),
  initialValue
)

const pipe = (...functions) => initialValue => functions.reduce(
  (acc, fn) => fn(acc),
  initialValue
)

const array = [-1, 1, -2, 2, -3, 3, -4, 4];

const mapDouble = array => array.map(x => x * 2);
const filterEven = array => array.filter(x => x % 2 === 0);

const composedResult = compose(mapDouble, filterEven)(array);
const pipedResult = pipe(mapDouble, filterEven)(array);

console.log('Composed result: ', composedResult); // [-4, 4, -8, 8]
console.log('Piped result: ', pipedResult); // [-2, 2, -4, 4, -6, 6, -8, 8]

Totally different result! This is why the pipe function should pass in operations in the reverse order of the compose function if you want the return values to be the same. This makes sense because the compose function uses the reduceRight method in its implementation, and the pipe function uses the reduce method instead.

Conclusion

We have now seen an advanced use case of the reduceRight method in JavaScript. We can use this method to create a compose function that performs function composition on a list of functions or operations. The compose function will be very useful when we talk about transducers in the next lesson.

Resources