The Array.reduce Method Part 5 - Making a Compose Operation
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.
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:
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:
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.
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:
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.
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.
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:
// 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.
| iteration | accumulator | current value | return value |
|---|---|---|---|
| 1st | -1, 1, -2, 2, -3, 3, -4, 4 | mapDouble | -2, 2, -4, 4, -6, 6, -8, 8 |
| 2nd | -2, 2, -4, 4, -6, 6, -8, 8 | filterPositive | 2, 4, 6, 8 |
| 3rd | 2, 4, 6, 8 | filterEven | 2, 4, 6, 8 |
| 4th | 2, 4, 6, 8 | mapTriple | 6, 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.
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.
| iteration | accumulator | current value | return value |
|---|---|---|---|
| 1st | -1, 1, -2, 2, -3, 3, -4, 4 | filterEven | -2, 2, -4, 4 |
| 2nd | -2, 2, -4, 4 | mapDouble | -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.
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.