- what is function composition
- composing simple functions
- composing asynchronous functions
In this short article I am going to discuss the function composition and how the function composition might work with asynchronous code. I'm going to explain the basic principles of it and give some examples.
Intro - Function composition
Javascript is a function friendly language. What we do often is to apply multiple functions in sequence. Some can say we are chaining the functions,hence the operator called pipe is often used or some say we are composing functions. This allows for clearly defined flow control and it makes a for a robust and testable code.
h(g(f(x)))
//logical representation
x -> f -> g -> h
We start with x and we apply functions f, g, h in that sequence. Function composition is not commutative (although some functions can commute), thus the order how we apply functions matters. For more information on this topic see the sources at the bottom of the is article
Composing functions in JavaScript
In this section we go from the basic function composition in JavaScript to more robust approach.
Example of function composition using synchronous functions:
const double = x => x + x;
const square = x => x * x;
const half = x => x / 2;
const dsh = (x) => half(square(double(x)))
This works nicely, but it is obviously hard to read when we have even slightly more complex logic. Something like this:
const hardToRead = (x) => cleanup(Calculate(params)(evaluate['@@space'](env)(getResult(x))))
is already much harder to read, even if we try a little tidying up.
const hardToRead = (x) => cleanup(
Calculate(params)(
evaluate['@@space'](env)(
getResult(x)
)
)
)
we would have to break some linting rules in order to give it some fasion.
const hardToRead = (x) => cleanup(
Calculate(params)(
evaluate['@@space'](env)(
getResult(x)
)))
// calling the function
hardToRead(x)
it looks good, but it has few downsides. We have to read it from bottom to top which is a little counter intuitive and it is not very flexible form. What if we decide that we want to add something after the cleanup operation? Are we going to rewrite it like this?
const hardToRead = (x) => logIt(
cleanup(
Calculate(params)(
evaluate['@@space'](env)(
getResult(x)
))))
// calling the function
hardToRead(x)
It is all doable, although we would need to be careful about number of brackets at the end.
However we can do more, we can introduce a helper function to help us with the function composition. With that the above piece of code can be written this way:
const hardToRead = pipe(
getResult,
evaluate(env),
calculate(params),
cleanup,
logIt // now adding extra functionality does not require a lot of rewritting
)
// calling the function
hardToRead(x)
The benefit of helper composition function is evident. The syntax is a lot cleaner. We can read the steps from top to bottom and we can add and remove any step without counting the closing brackets at the end. In addition function pipe is what is called higher order function. It returns another function which can be named and passed along or executed on the spot. Under the hood the pipe function is actually very simple and it does basically the same thing as the calling functions in sequence. It could look like this:
function pipe(...fns) {
return function(arg) {
return fns.reduce((acc, fn) => {
return fn(acc);
}, arg)
}
}
In practice the function composition is already built in JavaScript and conceptually it can be seen as reducing a collection of functions and over an initial parameter into a new value. Basically, all we are doing is taking the output value from previous operation as an input value of the next operation just like in the schematic diagram in the beginning. At the end we have the final result.
Asynchronous code
Composing only synchronous operation sometimes wouldn't get us too far. JavaScript is event driven programming language and asynchronous operation are at the heart of it. Composing asynchronous code is surprisingly straight forward as well.
We can leverage already built in common constructs - Promises. In the asynchronous world the already mention code could be written as follow:
getResult(url)
.then(evaluate(env))
.then(calculate(params))
.then(cleanup)
That is already pretty neat and personally I would use it as is as often as I can. So would we need another way to compose asynchronous functions? Let me explain. Sometimes we need to define the set of unique sequences of functions which might not even be known during the static evaluation. For example, in one path of the execution we would want to run:
getResult > eval_1 > eval_2 > calculate(param) > cleanup
and in the other path we want:
getResult > eval_1> eval_2 > eval_3 > calculate(param) > cleanup
or somewhere else we have:
getResult > eval_1> .... > eval_N > calculate(param) > cleanup
Moreover we could have another dynamic way of defining the number and order of the composed operations.
It is easy to see that chaining promises could become cumbersome and we need some help to create the composition. We can take the pipe
function from sync section and tweak it a little. Or a little more since the current implementation does not support await in Array.reduce
. However, it as long the await keyword is called inside async block any plain loop will wait for promise resolution. We can leverage:
function asyncPipe(...fns) {
return async function(arg) {
let res = arg;
for (fn of fns) {
res = await fn(res);
}
return res;
}
}
The pipe function in this implementation can accept both, synchronous and asynchronous function. To tackle the above challenge we could use it as follows:
const path = [method1, method2, ..., methodN];
const doPath = (path:Array<Function>) => pipe(
getResult,
...path,
calculate(params),
cleanup
)
const myUniquePath = doPath(path)
Now we can easily to chain the functions returning promises also in runtime when the set of required operations are not known at compile time.
Handling Exceptions?
What about catch block? Did we forget something? What if something goes wrong and we have to provide failed path option?
No news here. The asynchronous function is only a function returning promise so we have two main ways of handling this.
- traditional catch block in promises
- inside asynchronous block of code we have the option using try - catch construct.
doPath(url)
.then(result => { doSomethingWithResult(result) })
.catch(error => { doSomethingWithError(error) })
or
async asyncBock() {
try {
let res = await doPath(url)
doSomethingWithResult(res)
} catch(e) {
doSomethingWithError(e)
}
}
Advantages using function composition
In the ideal world of functional programming the function is completely decoupled from the environment where it runs. This makes it very easy to test as there is virtually no difference how the function is executed in the mocked test environment, the development environment and in the production environment. The function behaves exactly the same. Dividing the logic into independent steps gives the opportunity to combine these steps into more a complex operation without increasing the complexity of building stones and without an extra strain to increase complexity of our test environment.
Conclusion
Function composition is one of the foundation stones of functional programming. In this article we explained the basic rules of function composition and has shown how to apply composition of synchronous and asynchronous functions. It also outlined the basic implementation details leveraging the built in JavaScript language construct.
Further reading
There is a lot of existing libraries offering the pipe or function composition is some shape. I've successfuly used ramda. Others are happy with lodash/fp If somebody is interested in joining discussion there is a proposal for pipes as part of javascript syntax. hackpipes.
Sources
function composition ramda hackpipes image sourced from pixabay