1

How can I have a JavaScript function let's say piper() which takes several functions as its arguments and it returns a new function that will pass its argument to the first function, then pass the result to the second, then pass the result of the second to the third, and so on, finally returning the output of the last function.

Something like piper(foo, fee, faa)(10, 20, 30) would be equivalent to calling faa(fee(foo(10,20,30))).

ps: It was a part of an interview, that I did few days ago.

Mike Cluck
  • 31,869
  • 13
  • 80
  • 91
Ali Saberi
  • 864
  • 1
  • 10
  • 33
  • 1
    Have you tried anything yourself? – Mike Cluck Oct 12 '16 at 20:24
  • http://ramdajs.com/docs/#pipe – Jared Smith Oct 12 '16 at 20:25
  • @MikeC I am used to see functions in js like this function ali( saberi,i) {saberi(i)}, not in this way.with 2 paranthesis – Ali Saberi Oct 12 '16 at 20:27
  • 1
    @AliSaberi Remember: you can return functions from a function. [See this question](http://stackoverflow.com/questions/40007211/calling-function-with-two-separate-argument-objects#). – Mike Cluck Oct 12 '16 at 20:31
  • 1
    Like a lot of interview questions, if you ever code like this you deserve to fail a code review. This is absolutely nuts. `f(...)(...)(...)` is not readable at all. As an academic exercise the hints are in your question: it "returns a new function". – tadman Oct 12 '16 at 20:33
  • underscore.js. has a `_.compose()` function that does this. – Barmar Oct 12 '16 at 20:36
  • @tadman I'm not surprised you find `f (...) (...) (...)` as a ruby/php programmer, but your opinion on readability is purely subjective. Function composition, currying, and higher-order functions allow abstraction over arity which is a very powerful concept and one that permits some very expressive programs. You shouldn't dismiss it before developing an understanding of the domain you're commenting on. – Mulan Oct 12 '16 at 23:17
  • @naomik Just because you *can* do something doesn't mean it's a good idea. It's like having a function with sixty arguments. You can do it, nothing's stopping you, but it's a nightmare to get all the arguments in the correct order. This sort of chaining is an anti-pattern unless you have a very narrow use case where it's actually simpler than the alternative. I do mostly Ruby, Node, Swift and Rust these days, PHP is just incidental and unavoidable. – tadman Oct 12 '16 at 23:20
  • @tadman so in Haskell, where every function has one parameter, is somehow bad because it may require some specific syntax in order to apply a sequence of functions? Javascript `f (a) (b) (c)` vs Haskell `f a b c` is no more a bad program just because you think the JS one is "unreadable" – it's the exact same program; just with a syntactic difference. So sure, you can write bad javascript and you can write bad Haskell, but just because *you* find a particular program unreadable does not make the program bad or a program that is expressed incorrectly. – Mulan Oct 12 '16 at 23:24
  • @naomik I'm not even going to get started about Haskell. It's highly opinionated and able to get away with things that other languages prefer not to do. I've just given my opinion here, and like all opinions, it's a matter of perspective. Your case is an interesting demonstration of how this might be applied, but I think Haskell's use of this is a lot cleaner than JavaScript ever will be. – tadman Oct 12 '16 at 23:26
  • @tadman fwiw, I don't care much for Haskell, but it's not a bad language simply because it chose to be strict/opinionated about certain things; it solves problems in its own way and there's an active community to embrace the utility it offers. My point is that sequential function calls alone are not enough to indicate that a program is bad or "nuts". Yes, the syntax might look a little weird in JS, but that really doesn't matter. As someone that's unfamiliar with a particular *style* of programming, you cannot dismiss it on reading preference alone... – Mulan Oct 12 '16 at 23:47
  • ... Anyway, I only raise issue with your comment because I think there's a better way to communicate your idea in a learning community like SO. You can say "`f (...) (...) (...)` is difficult to read for some and might benefit from a code refactor" – it's much more constructive and leaves completely unsupported opinions out of the mix. – Mulan Oct 12 '16 at 23:50
  • @naomik It's just opinion here that it's hard to read, as unfamiliar syntax and conventions always is. Maybe this will become more mainstream, I've seen some *very* quirky conventions in Angular for example, but until it does it will require some explaining which is usually a bad thing. – tadman Oct 13 '16 at 01:32

7 Answers7

10

For an arbritrary number of functions you could use this ES6 function:

function piper(...fs) {
    return (...args) => fs.reduce((args,f) => [f.apply(this,args)],args)[0];
}
// Example call:
var result = piper(Math.min, Math.abs, Math.sqrt)(16, -9, 0)
// Output result:
console.log(result);

The same in ES5 syntax:

function piper(/* functions */) {
    var fs = [].slice.apply(arguments);
    return function (/* arguments */) { 
        return fs.reduce(function (args,f) {
            return [f.apply(this,args)];
        }.bind(this), [].slice.apply(arguments))[0];
    }.bind(this);
}
// Example call:
var result = piper(Math.min, Math.abs, Math.sqrt)(16, -9, 0)
// Output result:
console.log(result);
trincot
  • 317,000
  • 35
  • 244
  • 286
  • `f(...args)` would be shorter, but I like that you made it preserve context. – Jared Smith Oct 12 '16 at 20:39
  • So basically, It's ES6 problem and solution, right? – Ali Saberi Oct 12 '16 at 20:41
  • @Vic is right. This is not an ES6 *problem* (is there such a thing?), but a solution represented in ES6 syntax. – trincot Oct 12 '16 at 20:44
  • Added ES5 equivalent to my answer. – trincot Oct 12 '16 at 20:49
  • ES6 solution pins `this` to the global scope because of the arrow functions. Which is not good. ES5 example leaks `arguments`, which deoptimize the function. Otherwise, it's fine. – gaperton Oct 12 '16 at 21:00
  • @gaperton, in the ES6 solution, `this` refers to the context of the `piper` function. https://jsfiddle.net/dy03q38x/ – trincot Oct 12 '16 at 21:07
  • @trincot, sure, I know. Which will point to `window` in the browser. Which is the global scope. Which makes is impossible to use this function for methods chaining. The bug. – gaperton Oct 12 '16 at 21:10
  • @gaperton, did you check my fiddle (I added "use strict" here: https://jsfiddle.net/dy03q38x/1/)? I think it demonstrates quite well that `this` does not refer to `window` when you provide a context? – trincot Oct 12 '16 at 21:14
  • Nope. Does it demonstrate that capturing 'this' with arrow function and thus loosing the context is not a bug? :) – gaperton Oct 12 '16 at 21:16
  • It was never my intention in the ES6 version to capture context with the arrow function, but to pass the context `piper` was called with to the functions that were passed as arguments to it. The fiddle demonstrates that this is indeed happening. – trincot Oct 12 '16 at 21:20
  • I don't understand your point. Your piper implementation is designed to be invoked with call or apply (and bind the supplied context inside)? Well, great then. – gaperton Oct 12 '16 at 21:26
  • Indeed, that's it, or better put: it is designed so it *can* be used *also* with `call` or `apply` making use of the context argument when this is done. It is of course optional. – trincot Oct 12 '16 at 21:36
  • Interesting design choice. Considering the fact, that you can always bind context optionally with .bind. There is no any rational reason to do it intentionally inside of the functional combinators. – gaperton Oct 12 '16 at 21:47
  • It is a shorter way than to have to bind each of the functions you pass to `piper` separately. But even if that is not considered an advantage,it is not that much extra to mention `this` in the code the way I did it, so instead of asking *why*, I figured *why not*. ;-) Again, it is optional, much like the `contextArg` in the standard JavaScript `forEach`, and the likes. – trincot Oct 12 '16 at 22:08
  • "It is a shorter way than to have to bind each of the functions you pass to piper separately" -- if you do it right, you shouldn't do it separately. Just bind the result. piper( fun ) must return fun untouched, without any binding applied to it, and piper( f1, f2, f3 ) must do the same. – gaperton Oct 19 '16 at 18:09
  • So, in the right implementation piper( f1, f2, f3 ).bind( context ) should do the job. Apparently, it's not true about yours. – gaperton Oct 19 '16 at 18:11
  • What do you mean "should do the job"? Maybe you could provide a fiddle to illustrate what the issue is you want to highlight? – trincot Oct 19 '16 at 18:23
2

Enjoy. Pure ES5 solution. Preserves this.

function piper(){
    var i = arguments.length,
        piped = arguments[ --i ];

    while( --i >= 0 ){
        piped = pipeTwo( arguments[ i ], piped );
    }

    return piped;
}

function pipeTwo( a, b ){
    return function(){
        return a.call( this, b.apply( this, arguments ) );
    }
}

Or, if you want the fancy solution.

function piperES6( ...args ){
    return args.reverse().reduce( pipeTwo );
}

Loops can be reversed depending on the desired direction.

gaperton
  • 3,566
  • 1
  • 20
  • 16
1

Very similar to @trincot's answer (preserves context), but composes in the correct order and is marginally faster since it does not create intermediary arrays:

const piper = (...steps) => function(...arguments) {
  let value = steps[0].apply(this, arguments);
  for (let i = 1; i < steps.length; ++i) {
    value = steps[i].call(this, value);
  }
  return value;
};



// Usage:

let p = piper(
  x => x + 1,
  x => x * 2,
  x => x - 1
);

console.log(p(2)); // 5
Community
  • 1
  • 1
1

Here is an alternative answer involving method chaining. I shall use ES6, though of course this can be transpiled to ES5. On benefit of this solution is that is has a very succinct TypeScript counterpart with perfect typeability.

class Pipe {
    constructor(value) {
        this.value = value;
    }
    then(f) {
        return new Pipe(f(this.value));
    }
}
    
const pipe = value => new Pipe(value);
    
// Example

const double = x => 2 * x;
    
pipe(42).then(double).then(console.log); // 84

const result = pipe(42).then(double).then(double).value;
console.log(result); // 168
Carsten Führmann
  • 3,119
  • 4
  • 26
  • 24
0

A simple solution based on JS higher-order functions usage:

function pipe(...rest) {
  return x => rest.reduce((y, f) => f(y), x);
}

Usage:

pipe((a) => a + 1, (a) => a * 2)(3) // 8
pipe((a) => a + 1, (a) => a * 2)(2) // 2
Purkhalo Alex
  • 3,309
  • 29
  • 27
-3
function f(f1, f2, f3){

    return (args => f3(f2(f1(args))));

}
Isukthar
  • 185
  • 8
-3

I think what you are trying to do is chaining.

var funct={
    total:0,
    add:function(a) {
        console.log(funct.total,funct.total+a);
        funct.total+=a;

        return funct;
    }
};


funct.add(5).add(6).add(9);