Note: Before continuing to read this article, I recommend that you learn about What is Curry function? A delicious "curry" dish and how to enjoy it? beforehand or, if you already know about curry function, you can continue reading the article.
Composition is a mechanism that combines multiple simple functions to build a more complex function. The result of each function will be passed to the next function.
It is similar to in mathematics where we have a function f(g(x))
, which means the result of g(x)
is passed to the function f
. That's what composition is.
Let's take a simple example: write a function to perform the calculation 1 + 2 * 3.
For this calculation, we have to perform the multiplication first and then the addition. The code when implemented using functions in JavaScript will look like this:
const add = (a, b) => a + b;
const mult = (a, b) => a * b;
add(1, mult(2, 3));
Oh! the code runs fine but it's a bit messy, isn't it? What if I want to divide everything by 4 now? The code will look like this:
div(add(1, mult(2, 3)), 4);
It's even messier now!
Now let's move on to another example. Suppose I have a list of users
consisting of names and ages, let's get the names of people over 18 years old. The code for that would look like this:
const users = [
{ name: "A", age: 14 },
{ name: "B", age: 18 },
{ name: "C", age: 22 },
];
const filter = (cb, arr) => arr.filter(cb);
const map = (cb, arr) => arr.map(cb);
map(u => u.name, filter(u => u.age > 18, users)); // ["C"]
The idea is that I will create 2 functions filter
and map
, where filter
is used to filter and map
is used to iterate through the elements. The code above works fine, but like the first example, it's a bit cumbersome.
So, is there a way to solve both of these issues? Or at least make the code clearer when the problem conditions become more complex?
compose
functionMy goal is to create a function that takes multiple parameters, which are smaller functions to perform a certain amount of work (Higher Order Function).
It will look like this:
compose(function1, function2…, functionN): Function
compose
takes in functions and returns a function. The idea of compose
is that when it is called, it will execute the functions in the parameter from right to left, where the result of the previous function will be passed as an argument to the next function.
Here is a simple way to implement the compose
function in ES6:
const compose = (...functions) => args => functions.reduceRight((arg, fn) => fn(arg), args);
It would make me very happy if you understand what compose
really does inside, but if you don't understand, please comment below the article. I will follow your comment!
Now let's go back to the original example, let's modify the code a bit:
const add = a => b => a + b;
const mult = a => b => a * b;
const operator = compose(add(1), mult(2));
const result = operator(3);
// Or we can even shorten it
const result = compose(add(1), mult(2))(3);
I have turned add
and mult
into curried functions, why? Because when I turn them into curry, I can use them as a function parameter in compose
.
Alright, now what if I want to divide everything by 4?
const div = a => b => b / a;
const result = compose(div(4), add(1), mult(2))(3);
It's easy to read, isn't it? From left to right, perform multiplication by 2, then add 1, and finally divide by 4. It's like a flow, isn't it?
Similarly, with example 2, let's modify its code a bit:
const users = [
{ name: "A", age: 14 },
{ name: "B", age: 18 },
{ name: "C", age: 22 },
];
const filter = cb => arr => arr.filter(cb);
const map = cb => arr => arr.map(cb);
compose(
map(u => u.name),
filter(u => u.age > 18),
)(users); // ["C"]
We can add more consecutive functions inside compose
and still maintain the working data flow, and most importantly, keep the code relatively easy to read.
pipe
functionSimilar to compose
, pipe
has the same idea as compose
except that the order of executing functions in the parameter is from left to right.
The pipe
function will be implemented like this:
const pipe = (...functions) => args => functions.reduce((arg, fn) => fn(arg), args);
Applying pipe
to example 1:
const add = a => b => a + b;
const mult = a => b => a * b;
const result = pipe(mult(2), add(1))(3);
As you can see, the parameters are passed in reverse compared to compose
, it will perform the functions from right to left: multiply 3 by 2, then add 1.
You can use compose
and pipe
depending on your preference or habit as both functions produce similar results.
Both compose
and pipe
functions, though small, bring great benefits in applying them to problems of processing continuous data. They help the code to be clearer and easier to read.
Most JavaScript libraries that support data processing already have similar functions like compose
or pipe
, such as _.compose
, _.pipe
in lodash
, or compose
, pipe
in ramdajs
.
I hope that after this article, you will have another method to process data in upcoming projects. If you encounter similar problems now, try applying it immediately!
Me & the desire to "play with words"
Have you tried writing? And then failed or not satisfied? At 2coffee.dev we have had a hard time with writing. Don't be discouraged, because now we have a way to help you. Click to become a member now!
Subscribe to receive new article notifications
Comments (0)