DEV Community

Cover image for JS Functional Concepts: Pipe and Compose
JoelBonetR 🥇
JoelBonetR 🥇

Posted on • Updated on

JS Functional Concepts: Pipe and Compose

Function piping and composition are concepts from functional programming that of course are possible in JavaScript -as it's a multi-paradigm programming language-, let's deep into this concepts quickly.

The concept is to execute more than a single function, in a given order and pass the result of a function to the next one.

You can do it ugly like that:

function1(function2(function3(initialArg)))
Enter fullscreen mode Exit fullscreen mode

Or using function composition

compose(function3, function2, function1)(initialArg);
Enter fullscreen mode Exit fullscreen mode

or function piping

pipe(function1, function2, function3)(initialArg);
Enter fullscreen mode Exit fullscreen mode

To make it short, composition and piping are almost the same, the only difference being the execution order; If the functions are executed from left to right, it's a pipe, on the other hand, if the functions are executed from right to left it's called compose.

A more accurate definition would be: "In Functional Programming, Compose is the mechanism that composes the smaller units (our functions) into something more complex (you guessed it, another function)".

Here's an example of a pipe function:

const pipe = (...functions) => (value) => {
    return functions.reduce((currentValue, currentFunction) => {
      return currentFunction(currentValue);
    }, value);
  };
Enter fullscreen mode Exit fullscreen mode

Let's add some insights into this:

Basics

  • We need to gather a N number of functions
  • Also pick an argument
  • Execute them in chain passing the argument received to the first function that will be executed
  • Call the next function, adding as argument the result of the first function.
  • Continue doing the same for each function in the array.
/* destructuring to unpack our array of functions into functions */
const pipe = (...functions) => 
  /* value is the received argument */
  (value) => {
    /* reduce helps by doing the iteration over all functions stacking the result */
    return functions.reduce((currentValue, currentFunction) => {
      /* we return the current function, sending the current value to it */
      return currentFunction(currentValue);
    }, value);
  };
Enter fullscreen mode Exit fullscreen mode

We already know that arrow functions don't need brackets nor return tag if they are returning a single statement, so we can spare on keyboard clicks by writing it like that:

const pipe = (...functions) => (input) => functions.reduce((chain, func) => func(chain), input);
Enter fullscreen mode Exit fullscreen mode

How to use

const pipe = (...fns) => (input) => fns.reduce((chain, func) => func(chain), input);

const sum = (...args) => args.flat(1).reduce((x, y) => x + y);

const square = (val) => val*val; 

pipe(sum, square)([3, 5]); // 64
Enter fullscreen mode Exit fullscreen mode

Remember that the first function is the one at the left (Pipe) so 3+5 = 8 and 8 squared is 64. Our test went well, everything seems to work fine, but what about having to chain async functions?

Pipe on Async functions

One use-case I had on that is to have a middleware to handle requests between the client and a gateway, the process was always the same (do the request, error handling, pick the data inside the response, process the response to cook some data and so on and so forth), so having it looking like that was a charm:

export default async function handler(req, res) {
  switch (req.method) {
    case 'GET':
      return pipeAsync(provide, parseData, answer)(req.headers);
     /* 
       ... 
     */ 
Enter fullscreen mode Exit fullscreen mode

Let's see how to handle async function piping in both Javascript and Typescript:

JS Version

export const pipeAsync =
  (...fns) =>
  (input) =>
    fns.reduce((chain, func) => chain.then(func), Promise.resolve(input));
Enter fullscreen mode Exit fullscreen mode

JSDoc Types added to make it more understandable (I guess)

/**
 * Applies Function piping to an array of async Functions.
 * @param  {Promise<Function>[]} fns
 * @returns {Function}
 */
export const pipeAsync =
  (...fns) =>
  (/** @type {any} */ input) =>
    fns.reduce((/** @type {Promise<Function>} */ chain, /** @type {Function | Promise<Function> | any} */ func) => chain.then(func), Promise.resolve(input));
Enter fullscreen mode Exit fullscreen mode

TS Version

export const pipeAsync: any =
  (...fns: Promise<Function>[]) =>
  (input: any) =>
    fns.reduce((chain: Promise<Function>, func: Function | Promise<Function> | any) => chain.then(func), Promise.resolve(input));
Enter fullscreen mode Exit fullscreen mode

This way it will work both for async and non-async functions so it's a winner over the example above.

You may be wondering what about function composition, so let's take a gander:

Function Composition

If you prefer to call the functions from right to left instead, you just need to change reduce for redureRight and you're good to go. Let's see the async way with function composition:

export const composeAsync =
  (...fns) =>
  (input) =>
    fns.reduceRight((chain, func) => chain.then(func), Promise.resolve(input));
Enter fullscreen mode Exit fullscreen mode

Back to the example above, let's replicate the same but with composition:

How to use

const compose = (...fns) => (input) => fns.reduceRight((chain, func) => func(chain), input);

const sum = (...args) => args.flat(1).reduce((x, y) => x + y);

const square = (val) => val*val; 

compose(square, sum)([3, 5]); // 64
Enter fullscreen mode Exit fullscreen mode

Note that we reversed the function order to keep it consistent with the example at the top of the post.

Now, sum (which is at the rightmost position) will be called first, hence 3+5=8 and then 8 squared is 64.


If you have any question or suggestion please comment down below

Oldest comments (48)

Collapse
 
brense profile image
Rense Bakker

Nice explanation and code examples. I still think you should ditch jsdoc though 😜 that said... Array reducers are the only time I dislike typescript... It always produces a type collision between the initial value for the reducer and the current value, so you always have to give the initial value an explicit type, instead of relying on type inference, which I prefer, because having to make changes in explicit typing (when they aren't for data models) sucks imho. Not sure if it would be technically possible for typescript to infer the type from the return value of the reducer function though... Probably not.

Collapse
 
joelbonetr profile image
JoelBonetR 🥇 • Edited

Hahaha I prefer TS unless it's a quick script tbh, but IRL there are so many projects without TS that I feel like necessary to spread a bit of JSDoc (it makes it a lot easier to migrate JS to TS).

Type inference would be possible I guess but when you have functions that return different types then it will be a mess, I can imagine something like:

const userAge = getUserAge(); // inferred number
pipe( calcYearsBeforeRetirement, generateEmailBody, otherFunc )(userAge)
Enter fullscreen mode Exit fullscreen mode

userAge is a number, then you calc the amount of years he needs to work before he can retire, which can also be a number, everything OK here, then you cast this number as string inside the email body so the return type is not a number anymore but a string, and so on and so forth.

Hence I assume that even if TS tried, most of the time it could be wrong idk 😅

Collapse
 
apotre profile image
Mwenedata Apotre

I just knew pipe and now I know it differs to compose by just order of function execution! Thanks

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

Anytime 😁

Collapse
 
dmass profile image
Douglas Massolari • Edited

That's not always true, though.
In Elm, for example, pipe |> and composition >> have the same order of execution, the difference is that pipe is imediately executed while composition returns a new function:
Note: Everything after -- is a comment in Elm

add x y =
  x + y

sub y x =
  x - y

totalWithPipe =
  1
    |> add 10
    |> sub 5 -- 6

totalWithComposition =
  let
    calculateTotal =
      add 10 >> sub 5 -- returns a function
  in
  calculateTotal 1 -- 6
Enter fullscreen mode Exit fullscreen mode

Not only in Elm, but pipe in fp-ts also works this way:

import { pipe } from 'fp-ts/function'

const len = (s: string): number => s.length
const double = (n: number): number => n * 2

// without pipe
assert.strictEqual(double(len('aaa')), 6)

// with pipe
assert.strictEqual(pipe('aaa', len, double), 6)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
joelbonetr profile image
JoelBonetR 🥇

That's why the post has the tag #javascript 😁, either way love the insight! I haven't coded in Elm in ages, actually a good one, absolutely love the no runtime errors 🤩

It's sad that it got relatively few support...

Thread Thread
 
dmass profile image
Douglas Massolari

Yes, but even in Javascript this concept can be different as you can see in fp-ts’ pipe.

I love coding in Elm! It is my first option when creating a Frontend.

From what I see, it seems some companies are adopting it, so, it seems it’s growing

Thread Thread
 
joelbonetr profile image
JoelBonetR 🥇 • Edited

I picked the mathematical explanation for function composition:

In abstract algebra, a composite function is a function formed by the composition or successive application of more than one function. To do this, the function closest to the argument is applied to the argument, and the next function is applied to the result of the previous calculation.

in which case, this will fit in the description:

compose(function3, function2, function1)(initialArg);
Enter fullscreen mode Exit fullscreen mode

The implementation details or nuances in Elm (or any other) is a different matter of discussion 😁

BTW glad to hear Elm it's getting a bit more love!

Thread Thread
 
dmass profile image
Douglas Massolari

You are right.
But the point of my comment is pipe.
This is the one that have different implementations.
I just highlighted that the affirmation “pipe is the same as composition but reversed” is not always true

Thread Thread
 
joelbonetr profile image
JoelBonetR 🥇

oh! understood now 😁 my bad

Collapse
 
joshuakb2 profile image
Joshua Baker

I'd like to point out that the pipeAsync and composeAsync functions are examples of monadic composition! 😁

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

Indeed they are! 👌🏼😁

Collapse
 
jwp profile image
John Peters

Nice Joel, thanks 😊

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

Thank you for reading John! 😉

Collapse
 
edlinkiii profile image
Ed Link III

IMHO, the "ugly way" is a lot more intuitive and easier to read. Not that I don't appreciate the work you've done or the insight I gained from reading your article. ✌🏻

Collapse
 
joelbonetr profile image
JoelBonetR 🥇 • Edited

It's your opinion and when you use it in your projects, it will be your code, so use the style you prefer or feel more comfortable with, it's totally OK! 😁

Edit: I wrote the post in different days (one bit at a time) and just realized I had been using different wording for the references on those functions

So for this one:

const pipe = (...functions) => 
  (initialArg) => {
    return functions.reduce((currentValue, currentFunction) => {
      return currentFunction(currentValue);
    }, initialArg);
  };
Enter fullscreen mode Exit fullscreen mode

we could save few keyboard clicks by coding it like this:

const pipe = (...functions) => (initialArg) => 
  functions.reduce((currentValue, currentFunction) => {
      return currentFunction(currentValue);
    }, initialArg);
Enter fullscreen mode Exit fullscreen mode

Just like this in the last one:

export const pipeAsync: any =
  (...fns: Promise<Function>[]) =>
  (initialArg: any) =>
    fns.reduce((currentFunc: Promise<Function>, func: Function | Promise<Function> | any) => currentFunc.then(func), Promise.resolve(initialArg));
Enter fullscreen mode Exit fullscreen mode

Which is probably more... understandable?

Let me know, if it helps I can update the post! 😁

Collapse
 
naofumi666 profile image
Naofumi

I like this🔥

Collapse
 
alessioferrine profile image
alessioferrine

It's quite interesting things, thanks for writing about it

Collapse
 
fluxthedev profile image
John

Can someone give a few examples of where this might be useful in the real world? :)

Collapse
 
joelbonetr profile image
JoelBonetR 🥇 • Edited

Sure!
If you apply good practices and split the code in single-responsibility functions you'll end up chaining quite a few of them.

In OOP you'll do something like:

function replaceBy  (target, replacement) {
  return num.toString().split(target).join(replacement)
}
Enter fullscreen mode Exit fullscreen mode

Whereas in functional programming it will look something like:

const replaceBy = (target, replacement) =>
  pipe(toString, split, join)([target, replacement);
Enter fullscreen mode Exit fullscreen mode

Which is objectively better than

const replaceBy = (target, replacement) =>
  toString(split(join(target, replacement)));
Enter fullscreen mode Exit fullscreen mode

Specially as the chain grows and for readability: you can actually read them from left to right in comma separated names, plus having just one initial arg usually helps to avoid side-effects.

So it's not a niche concept but a generic one, a nice to have (and use).

That's a bit of a silly example but it may work just to showcase, also you can find another example in the post 🙂

Collapse
 
flyingcrp profile image
flyingCrp

yeah,
just need one package, caolan.github.io/async/v3/docs.htm...
In the magical world of JS,If one package cannot be solved, find another package

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

Why to add a dependency to the project for something that takes less than 5 LoC?

Collapse
 
flyingcrp profile image
flyingCrp

I believe the power of the community is greater than the individual And different developers have different abilities

Thread Thread
 
joelbonetr profile image
JoelBonetR 🥇 • Edited

But is the individual the one that would need to solve dependency compatibility issues, so choose wisely which ones are you going to import, because the less you add the more ease you'll be granted with during the maintenance of the project and further developments.

Thread Thread
 
flyingcrp profile image
flyingCrp

Yes, in the end, individuals are always dealing with these problems.

Collapse
 
luiyit profile image
Luiyit Hernandez

Very interesting, I also learned about curried functions. Thanks for sharing!

Collapse
 
gohomewho profile image
Gohomewho

WoW this is so cool!

In the last example, I think the pipe from pipe(square, sum)([3, 5]); should be compose.

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

Hi Gohomewho, thanks for pointing it out! I'm fixing it right now 🙂

Collapse
 
annabaker profile image
Anna Baker

Great Post!

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

Thank you Anna! 🙂

Collapse
 
vinsay11 profile image
Winsay vasva

good article

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

Thank you Winsay 😁

Collapse
 
daniilgrebenick profile image
daniilGrebenick

1

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
Sloan, the sloth mascot
Comment deleted
 
Sloan, the sloth mascot
Comment deleted

Some comments have been hidden by the post's author - find out more