JS Functional Concepts: Pipe and Compose
JoelBonetR 🥇

JoelBonetR 🥇 @joelbonetr

About: Tech Lead/Team Lead. Senior WebDev. Intermediate Grade on Computer Systems- High Grade on Web Application Development- MBA (+Marketing+HHRR). Studied a bit of law, economics and design

Location:
Spain
Joined:
Apr 19, 2019

JS Functional Concepts: Pipe and Compose

Publish Date: Jan 2 '23
159 49

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

Comments 49 total

  • Rense Bakker
    Rense BakkerJan 2, 2023

    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.

    • JoelBonetR 🥇
      JoelBonetR 🥇Jan 2, 2023

      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 😅

  • Mwenedata Apotre
    Mwenedata ApotreJan 2, 2023

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

    • JoelBonetR 🥇
      JoelBonetR 🥇Jan 2, 2023

      Anytime 😁

    • Douglas Massolari
      Douglas MassolariJan 4, 2023

      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
      • JoelBonetR 🥇
        JoelBonetR 🥇Jan 4, 2023

        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...

        • Douglas Massolari
          Douglas MassolariJan 4, 2023

          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

          • JoelBonetR 🥇
            JoelBonetR 🥇Jan 5, 2023

            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!

            • Douglas Massolari
              Douglas MassolariJan 5, 2023

              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

  • Joshua Baker
    Joshua BakerJan 2, 2023

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

  • JWP
    JWPJan 3, 2023

    Nice Joel, thanks 😊

  • Ed Link III
    Ed Link IIIJan 3, 2023

    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. ✌🏻

    • JoelBonetR 🥇
      JoelBonetR 🥇Jan 4, 2023

      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! 😁

  • Naofumi
    NaofumiJan 4, 2023

    I like this🔥

  • alessioferrine
    alessioferrineJan 4, 2023

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

  • John
    JohnJan 4, 2023

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

    • JoelBonetR 🥇
      JoelBonetR 🥇Jan 4, 2023

      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 🙂

  • flyingCrp
    flyingCrpJan 5, 2023

    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

    • JoelBonetR 🥇
      JoelBonetR 🥇Jan 5, 2023

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

      • flyingCrp
        flyingCrpJan 12, 2023

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

        • JoelBonetR 🥇
          JoelBonetR 🥇Jan 12, 2023

          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.

          • flyingCrp
            flyingCrpJan 13, 2023

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

  • Luiyit Hernandez
    Luiyit HernandezJan 5, 2023

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

  • Gohomewho
    GohomewhoJan 5, 2023

    WoW this is so cool!

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

    • JoelBonetR 🥇
      JoelBonetR 🥇Jan 5, 2023

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

  • Anna Baker
    Anna BakerJan 5, 2023

    Great Post!

  • Winsay vasva
    Winsay vasvaJan 6, 2023

    good article

  • daniilGrebenick
    daniilGrebenickJan 6, 2023

    1

  • АнонимJan 7, 2023

    [deleted]

  • fruntend
    fruntendJan 9, 2023

    Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
    Keep it up 👍

  • Mahmoud Harmouch
    Mahmoud HarmouchJan 10, 2023

    Thanks for sharing such a neat concept. Also, +1 on using JSDoc (Get the TS code outta my face. Kidding, keep it.).

    • JoelBonetR 🥇
      JoelBonetR 🥇Jan 10, 2023

      😂

      • Mahmoud Harmouch
        Mahmoud HarmouchJan 10, 2023
        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

        types aren't that useful

        • JoelBonetR 🥇
          JoelBonetR 🥇Jan 10, 2023

          To be fair in this situation it can, effectively, be any 😅
          The return type would be the return type of the last function called and the type to pass to the next function would be the output type of the one before 🤯 just in case you are up for writing a type function 😁

          • Mahmoud Harmouch
            Mahmoud HarmouchJan 10, 2023

            These situations always keep me thinking about why I am using TS in the first place. You will end up spending so much time figuring out the types in these tricky situations, which is NOT a business problem to work on in the first place. That's why I would love to revert back to JS, anytime.

            • JoelBonetR 🥇
              JoelBonetR 🥇Jan 10, 2023

              Because the absence of types (nor TS nor JSDoc+TS Pragma ) leads sooner or later to non-expected paths that can break the app in runtime, which is a business problem that will rain as sh*t over the dev team (Imagine you spent 200k on a marketing campaign and the app crashes avoiding the conversion that could eventually amortize the costs plus benefits).

              Are the types necessary in every single App? No, they aren't.

              Still it's recommended to have them in most apps. On the other hand, JSDoc + TS Pragma (remember that JSDoc alone does nothing but printing an informative text, you need TS Pragma to get type checks on dev time) is better than nothing, but TS has much more features than that.

              • Mahmoud Harmouch
                Mahmoud HarmouchJan 10, 2023

                Because the absence of types (nor TS nor JSDoc+TS Pragma ) leads sooner or later to non-expected paths that can break the app in runtime, which is a business problem that will rain as sh*t over the dev team

                Yep. Although the absence of types may not be considered a major problem by some, me included, I believe that not following best practices is what ultimately causes more issues in web development. By adhering to established conventions and standards, we can avoid many of the potential problems that can occur without these guidelines. In addition, following best practices often leads to code that is easier to read and understand, which can save time and frustration for all parties involved.

                But, the thing is that major business issues, even though I don't have data but from what I have experienced, are not necessarily caused by the absence of types. I think there is a correlation between the two. But, the relation is not causation. The absence of types doesn't necessarily lead to rain as sh*t over the dev team. Know what I am saying?

                Are the types necessary in every single App? No, they aren't.

                Agree.

                • JoelBonetR 🥇
                  JoelBonetR 🥇Jan 10, 2023

                  Of course not, the main issue is not having tests.
                  Key in the discussion here being that coding in TypeScript is faster than vanilla JS + JSDoc + TS Pragma, you may never seen it this way but look:

                  /**
                   * Sums two numbers
                   * @param  {number} n1
                   * @param  {number} n2
                   * @returns {number}
                   */
                  const sumTwoNumbers = (n1, n2) => n1+n2;
                  
                  Enter fullscreen mode Exit fullscreen mode
                  /** Sums two numbers */
                  const sumTwoNumbers = (n1: number, n2: number) => n1+n2;
                  
                  Enter fullscreen mode Exit fullscreen mode

                  As well as more reliable.
                  To get a similar reliability with JSDoc you need to ensure JSDoc is added and maintained through automatisms in the linter and run this step in the PR's pipeline, at least (i.e. ESLint plugin JSDoc) and it takes more to configure than what it takes to configure TS most of the time.

                  Keep in mind that using JSDoc and TSPragma you are just using one little piece of TS, which is about type definition and type reports (and it doesn't even cover it entirely).

                  If you just need those features, then add TS and just use those features 😂

                  Do you dislike interfaces? Fine, don't use them, if you are working in FP instead OOP, interfaces doesn't even make sense (in FP all functions are interfaces).

                  It is not mandatory to use everything from TS so don't stress it so hard 😁

  • Mihai Farcas
    Mihai FarcasJan 19, 2023

    Great article! Love the fact that you include the async versions.
    Glad to see people interested in this topic!
    I have a YT video on this same topic: youtube.com/watch?v=q1aNVIq3K7c
    where I show some real world examples. Feedback is very appreciated!

Add comment