When you want to do something but it’s quite complex to be in one function, how do you solve this problem? What if you want to separate the function into many pieces but you want to do something more elegantly?
There’s an interesting proposal on TC39 GitHub at stage 1. Although it’s still unsettled as it’s mentioning, the idea of the proposal is quite fun to take a look at.
In this post, I will talk about pipe or pipeline which is important when it comes to functional programming and how to write them.
This post is under the premise that you are aware of JavaScript basics and some of the built-in methods in the Array.prototype
.
If you aren’t familiar with those, I recommend you read the documentation about them. And, the way I make pipeline functions might be different from other pipelines in the JavaScript world. But the core concept wouldn’t be so different.
Plus, I will explain pipe
and how to create it step-by-step for people who have no idea about it.
So, first things first, let’s talk about what a pipe is in JavaScript. What do you think of when you hear “pipe”? Yes, you’re right. A bunch of pipes on the ceiling for gas or something, or under the ground for water.
What those pipes are for is to be a tunnel for resources to go from A to B. It doesn’t do anything to the resources except for being the space for the resources to pass through.
The idea of pipe functions in JavaScript came from the pipeline in the real world.
This is a really important concept for pipe. What is a pure function? A pure function is a function that does the things below.
But then why is a pure function important for pipe? As I explained above, pipe delivers a given value to the next pipe. But if the function could return a different value, then the next function in the pipeline can’t assure the stable result.
OK. Let’s take a look at a simple example.
const addFour = a => a + 4;
const minusFive = a => a - 5;
const nine = addFour(5);
const four = minusFive(nine);
We have two functions, addFour
and minusFive
. We can easily get a return from each function. And when we want to add and subtract numbers at once, we can do this:
addFour(minusFive(0)); // -1
Then, multiply by 10.
multiplyByTen(addFour(minusFive(0))); // -10
But since we don’t like a negative number, we want to change it to the positive.
Math.abs(multiplyByTen(addFour(minusFive(0)))); 10
What do you think of this process? Doesn’t this look quite messy? Especially as there are so many closing-parentheses at the end of the statement.
Of course, it doesn’t cause any programmatic errors or problems. But this may reduce the readability of codes.
To make pipe work, you should make the functions so they run consecutively. In the Array
class, there’s a very useful method for this, Array.prototype.reduce
. What reduce
does is to loop over an array and to make one compact result.
For example, if you want to get the total score of all students, you may write your function like this, below:
const scores = [90, 100, 40, 50, 10];
let total = 0;
scores.forEach(score => total += score);
console.log(total); // 290
This example used forEach
because map
or filter
produces a new array. But this example has a problem, because total
has to be declared with let
, since total
has been changed in the loop of forEach
.
In contrast, however, reduce
returns a value that has been calculated over the loop.
const scores = [90, 100, 40, 50, 10];
const total = scores.reduce((acc, cur) => acc + cur, 0);
console.log(total); // 290
const pipe = (funcs) => {
funcs.reduce((acc, cur) => {
...
});
}
OK, now what? Each argument of func.reduce
has to return the values that the next function requires.
From the perspective of the next function, the parameter that should be passed into it should come from the previous call of reduce
. In reduce
, acc
is the return value and cur
is the current value of an array.
Good. Then, let’s change the names of parameters for better readability.
funcs.reduce((res, func) => {
...
});
Now you know each return value will be the passed argument for the next function. To make this happen, we should run the function with the parameter, res
.
funcs.reduce((res, func) => {
return func(res);
});
But, this is still quite strange. Why? Even if the functions will be executed properly with the parameter they need, what if the very first function also wants to take the parameter?
In this example, since initialValue
of reduce
doesn’t exist, the first element of an array will be used as initialValue
and this will be a function and it will be omitted from execution.
Then, the second element of an array will be used as the first cur
of the loop.
So, you need to set the initialValue
for the first function in the array, but as what? You didn’t take any values into the pipe
.
InitialValue
We will take the value for the function.
const pipe = (funcs, v) => {
funcs.reduce((res, func) => {
return func(res);
}, v);
};
Now, let’s try to run this function to see if it works as expected.
const addFive = v => v + 5;
pipe([
add,
console.log
], 5);
// 10
It works! But, there’s actually a problem, because pipe
only takes an array. So, you can’t run the pipe
function like this, below:
pipe(add, 5);
// ~~~
// Uncaught TypeEror: funcs.reduce is not a function
It seems like it’d be better if pipe
can take a single function as well. At this point, I’m going to use the rest parameter.
What the rest parameter does is to allow a function to take any parameters as an array.
// Rest parameter must be last formal parameter !!
const pipe = (v, ...funcs) => {
funcs.reduce((res, func) => {
return func(res);
}, v);
};
Now, let’s see if it works.
const subtract = v => console.log(v - 5);
pipe(10, subtract); // 5
It works, but now the value passed into the pipe
for the first parameter of the first call should be the first parameter. Because we’ve changed the order of parameters due to the rule of the rest parameter.
I don’t like this. Then, how can we fix this?
We will use a closure. A closure is a function that returns a reference to the outer function that is already finished executing. If you aren’t familiar with closure, you might want to check out my previous post.
So, finally, the pipe
can be changed like this:
const pipe = (...funcs) => v => {
funcs.reduce((res, func) => {
return func(res);
}, v);
};
pipe(add)(5) // 10
Now, the pipe
returns another function that takes v
. Until the return function takes v
, it doesn’t fulfil reduce
. As the last step, let’s confirm if it works properly.
const minusFive = v => v - 5;
const addFour = v => v + 4;
const multiplyByTen = v => v * 10;
pipe(
minusFive,
addFour,
multiplyByTen,
Math.abs,
console.log
)(0); // 10
// it works !
pipe
is one of the very useful functions in functional programming. It reduces the complexity of reading your codes and, in contrast, increases the readability.
The important thing to remember is that each function that will be used with pipe
should be a pure function.
Thank you for reading, Please share if you liked it!
#JavaScript #Functional Programming #Coding #Web Development #Programming