Search code examples
javascriptecmascript-6ecmascript-5ecmascript-2016

Nice way to nest many function calls (Unix piping) in Javascript


I was looking for a way to do nested function calls nicely, to avoid something like:

var result = function1(function2(function3()));

Or something like:

var result = function3();
result = function2(result);
result = function1(result);

Something like piping from Unix would be nice:

var result = function3() | function2() | function1();

Source: https://www.npmjs.com/package/babel-plugin-operator-overload

Of course | is the bitwise or operation, this example is abstract.

Does anybody know of any way to achieve an effect like this using ES5, ES6, or ES7, without transpiling?

Edit

Thank you T.J Crowder, torazaburo, and Bergi, you all added unique, useful, and interesting information in your answers.


Solution

  • Without a helper function

    I took your question initially to be doing this without any helper function, but your subsequent comment suggests that that's not the case. Skip down if helper functions are in scope.

    Without adding any helper functions, you can use ES6 promises:

    Promise.resolve()
        .then(function3)
        .then(function2)
        .then(function1)
        .then(result => {
      console.log("result is " + result);
    });
    

    It's not prettier than

    var result = function1(function2(function3()));
    

    ...but at least the functions being called are listed in the order they're called, and promises are very quite flexible in multiple ways.

    E.g: Live copy on Babel's REPL

    function function1(arg) {
      console.log("function1 called with " + arg);
      return "result1";
    }
    function function2(arg) {
      console.log("function2 called with " + arg);
      return "result2";
    }
    function function3() {
      console.log("function3 called");
      return "result3";
    }
    
    Promise.resolve()
        .then(function3)
        .then(function2)
        .then(function1)
        .then(result => {
      console.log("result is " + result);
    });
    

    Output:

    function3 called
    function2 called with result3
    function1 called with result2
    result is result1
    

    With a helper function

    Re your comment:

    function pipe(){
        var str = 'Promise.resolve()';
        for(var i = 0; i < arguments.length; i++){
            str += '.then(arguments[' + i + '])'
        }
        eval(str);
    }
    
     pipe(c, b, a, result => { console.log("result is " + result); });
    

    I know pipe is a thing in fs libraries, so the function name isn't exactly great. Aside from that, is there anything glaringly wrong with this?

    If you want to throw a helper function at this, there's no need at all for eval. For non-promise-ified functions, just do:

    function pipe(first, ...more) {
      return more.reduce((r, f) => f(r), first());
    }
    

    and

    let result = pipe(function3, function2, function1);
    

    Live copy on Babel's REPL

    If you want to do this with promise-ified functions or a mix, then:

    function pipe(...functions) {
      return functions.reduce((p, f) => p.then(f), Promise.resolve());
    }
    

    then you can call it the way you showed:

    pipe(function3, function2, function1, result => {
        // ...
    });
    

    ...but doing so ignores errors. Since pipe returns the last promise, you can use all the promise goodness

    pipe(function3, function2, function1, result => {
        // ...
    }).catch(failure => {
        // ...
    });
    

    or

    pipe(function3, function2, function1)
        .then(result => {
            // ...
        })
        .catch(failure => {
            // ...
        });
    

    Here's a complete example mixing simple functions and functions that return promises: Live copy on Babel's REPL

    function pipe(...functions) {
        return functions.reduce((p, f) => p.then(f), Promise.resolve());
    }
    function function1(arg) {
        console.log("function1 called with " + arg);
        return "result1";
    }
    function function2(arg) {
        console.log("function2 called with " + arg);
        return new Promise(resolve => {
            setTimeout(() => {
                resolve("result2");
            }, 100);
        });
    }
    function function3() {
        console.log("function3 called");
        return "result3";
    }
    
    pipe(function3, function2, function1)
        .then(result => {
            console.log("Final result is " + result);
        })
        .catch(failure => {
            console.log("Failed with " + failure);
        });
    

    Output:

    function3 called
    function2 called with result3
    function1 called with result2
    Final result is result1