Functional programming is about developing small simple functions and combining them for the purposes of executing more complex tasks.
As the programâs logic is divided into small functions under the functional approach, they have to be assembled into larger functions for the application to work. This process is called composition.
Function composition (or function superposition) is a pinpoint application of one function to the result of another one, or in other words, the use of one functionâs result as the argument of another one.
For example:
function1(function2(value))
Each time, the result of the right-hand function in the chain is passed as a parameter to the function located in the left-hand side. Therefore, all the functions in the chain can have only one parameter, except for the rightmost one which is executed first.
Why is function composition needed?
Function composition has the following advantages:
- It shortens the code
- We can see the order of actions at once
- A link can be easily removed from the chain of functions
- If we consider React, it allows for more convenient wrapping of components around other components, which includes using a high-order component.
How do we use it?
There is no Ńompose
function in the native JavaScript or React. It can be obtained with the aid of third-party libraries. For example, it is present in Redux.
The syntax of its application looks like this:
compose(function1, function2)(function3);
Here, function3
will be executed first. And then one by one, from right to left, its result will be accepted and processed by funŃtion2
and function1
.
To obtain a more universal solution, we can use the reduceRight
method:
const compose = (...fns) => (initialVal) =>
fns.reduceRight((val, fn) => fn(val), initialVal);
Here are a few examples of how they can be used:
const add2 = (n) => n + 2;
const times2 = (n) => n * 2;
const times2add2 = compose(add2, times2);
const add2times2 = compose(times2,add2);
const add6 = compose(add2, add2, add2);
times2add2(2); // 6
add2tiems2(2); // 8
add6(2); // 8
You might think that it has no relation to front-end, but it is also helpful in SPA (single page application). For example, you can add behavior to the React component using higher-order functions:
function logProps(InputComponent) {
InputComponent.prototype.componentWillReceiveProps = function (nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
};
return InputComponent;
}
// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);
And what about pipe?
It is customary to use the abstraction tool called pipe()
for composition. With the aid of pipe
we obtain a declarative code, that is, the programmer describes the result and not the implementation.
Letâs consider the difference between the declarative and imperative approaches in more detail. Letâs assume there are 2 simple functions:
const g = n => n + 1;
const f = n => n * 2;
We want to combine them into one function, so that first one would be added to the number, and then the obtained result would be multiplied by 2.
With the imperative approach, the functionâs implementation would look like this:
const doImperative = x => {
const afterG = g(x);
return f(afterG);
};
doImperative(20); // 42
The declarative approach suggests that the auxiliary function pipe()
should be used. It is implemented by many popular libraries, such as lodash
, but in actual fact its primitive implementation is quite simple and can fit into one line of code:
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
Now, with the pipe()
function, the example above can be rewritten using the declarative approach:
const doDeclarative = pipe(g, f)
doDeclarative(20); // 42
The difference between the approaches becomes even more obvious when the task gets more complicated. Letâs add logging after each step with the aid of the function:
const trace = message => value => {
console.log(`${message}: ${value}`);
return value;
}
The imperative style:
const doImperative = x => {
const afterG = g(x);
trace('after g')(afterG);
const afterF = f(afterG);
trace('after f')(afterF);
return afterF;
};
doImperative(20);
The declarative style:
const doDeclarative = pipe(
g,
trace('after g'),
f,
trace('after f')
);
doDeclarative(20);
The doDeclarative()
method doesnât store the intermediary result in the variables, the result of the previous function is passed to the next one instead. The pipe
function is responsible for that.
It is interesting to note that the chain of Promises is a composition too:
Promise.resolve(20)
.then(g)
.then(trace('after g'))
.then(f)
.then(trace('after f'));
Each subsequent then
block accepts the previous oneâs result and passes it to the next one. Looks very much like the previously considered variant of pipe
, doesnât it?
Conclusion
Function composition improves code readability. Instead of function nesting, you can link the functions into a chain and create higher-order functions with meaningful names.
The implementation of compose
is present in many JavaScript utility libraries (lodash
, ramda
and so on).