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.