Understand and Use Pipe Function in JavaScript

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.

Before Reading

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.

  • Reduce in MDN
  • Arrow function in MDN

Plus, I will explain pipe and how to create it step-by-step for people who have no idea about it.

Definition of Pipeline in JavaScript

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.

Pure Functions

This is a really important concept for pipe. What is a pure function? A pure function is a function that does the things below.

  1. Returns the same output with the same input.
  2. Doesn’t change any values out of its function scope.

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.

Without Pipe

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.

How To Write Pipe

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

Step 1. Put all of the functions that we want to enqueue into reduce

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) => {
  ...
});

Step 2. Fill the body part

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.

Step 3. Set the 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

Step 4. Generalization

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.

Final step: Testing

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 !

Conclusion

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

Understand and Use Pipe Function in JavaScript
3 Likes198.60 GEEK