iammrharuna
iammrharuna

Reputation: 31

Given a function pipe(foo, bar, baz)(1, 2, 3), how would you implement it to be equivalent to baz(bar(foo(1,2,3)) in javascript

I'm currently learning javascript. I came across this question and tried solving it using currying in javacript but could not get it quite right.

Given a function pipe() that takes several functions as arguments and returns a new function that will pass its argument to the first function, then pass the result to the second, then to the third, and so on, returning the output of the last function. So given: pipe(foo, bar, baz)(1, 2, 3) for instance, would be equivalent to baz(bar(foo(1,2,3))).

How would I go about solving this in javascript?

Upvotes: 2

Views: 1249

Answers (6)

Mulan
Mulan

Reputation: 135377

Functional programming is fun, so I'll take a swing at this one. Generally, this concept is called function composition and it is best utilized on unary functions (functions that only take one argument).

At its most basic level, function composition looks like this

const comp = f => g => x => f (g (x))

And composing a list of functions is a fold of that list using comp starting with the id function.

const id = x => x;

const foldl = f => y => xs =>
  xs.length === 0 ? y : foldl (f) (f (y) (xs[0])) (xs.slice(1));

const compN = fs => foldl (comp) (id) (fs);

We can se this working using

let foo = x => x - 1;
let bar = x => x * 20;
let baz = x => x + 3;

compN ([foo, bar, baz]) (2);
//=> 99

So how is that working?

compN ([foo, bar, baz])
// returns: x => foo(bar(baz(x)))

So when we call (2) on that return value, 2 gets passed in as x and then the entire chain executes

foo(bar(baz(2)))
foo(bar(5))
foo(100)
//=> 99

As a result of this implementation, you get compN with 3 other reusable functions, id, comp, and foldl


However, your question is written with some magic in mind. Your criteria suggests that the function composition should take more than one argument at the entry, so that kind of bastardizes the whole concept by introducing an ugly abnormality. Anyway, here's a way you could write it

const id = x => x;

const foldl = f => y => xs =>
  xs.length === 0 ? y : foldl (f) (f (y) (xs[0])) (xs.slice(1));

const badComp = f => g => (...xs) => f(g(...xs));

const pipe = fs => foldl (badComp) (id) (fs);

pipe ([x => x * x, (x,y) => x + y]) (2,3); //=> 25

//=> (2+3) * (2+3)
//=> 5 * 5
//=> 25

As you can see, the usefulness here isn't even greatly improved. If you wanted to send multiple arguments to a function, you'd be off sending them as an array. So instead of foo(1,2) use foo([1,2]). That way you can use the much preferred unary function composition as described in the first part of my answer. Then you don't have to rely on toothache sugar like rest arguments (...xs) => and spread operator f(g(...xs)).

Also, ES6 gives you destructuring assignment, so you could easily use the unary function composition method and still write methods that appear to take multiple arguments. The pipe example could be rewritten with compN using

compN ([x => x * x, ([x,y]) => x + y]) ([2,3]);

So yeah, my opinions are clearly in favour of classical unary function composition but you can use whichever one makes you happy or completes your homework assignment.

Anyway, that's my own 2 cents. If you have any questions, I'm happy to help.


I didn't want to overwhelm you with the initial implementation of compN so I kept the foldl function simple. If I were to carry this out, I'd actually take it a little further.

const id = x => x;
const comp = f => g => x => f (g (x));
const eq = x => y => y === x;
const prop = x => y => y[x];
const len = prop ('length');
const isEmpty = comp (eq (0)) (len);
const first = xs => xs[0];
const rest = xs => (xs) .slice (1);
const foldl = f => y => xs =>
  isEmpty (xs) ? y : foldl (f) (f (y) (first (xs))) (rest (xs));
const compN = fs => foldl (comp) (id) (fs);

And of course it works the same

compN ([x => x - 1, x => x * 20, x => x + 3]) (2) //=> 99

As a result of this compN implementation, we get 9 other completely reusable functions for FREE. That's huge, imo. That means the next time you have to define another function, you're likely to have a good portion of the work already complete for you as defined by these other functions.

Also, I chose to implement foldl instead of depend on the native Array.prototype.reduce as not only does it show you how to implement iterative looping via recursion but also because it expects a curried procedure.

The equivalent of my foldl using native Array.prototype.reduce would be

var uncurry = f => (x,y) => f (x) (y);
var foldl2 = f => y => xs => xs.reduce(uncurry(f), y);

Either is fine. Again, choose whichever you like. If you end up choosing an Array.prototype.reduce solution, at least you know how to make your own now ^,^

Pretty radical stuff. OK good luck and good bye now.

Upvotes: 4

Kenan Banks
Kenan Banks

Reputation: 212078

More or less the correct way to do this, using monadic reduction/composition with an identity function. Note that there is no for loop.

// I = Identity function = simply pass through argument.
var I = function (x) {return x};
// Helper function: convert arguments to real array.
var my = function (x) {return Array.prototype.slice.call(x, 0)}

// Our test function.
var add1 = function (x) {return x + 1};

// "Pipe" a list of functions passing the output of each to the next.
function pipe (fs) {
  return fs.reduce(function (f, g) {
    return function () {
      return g(f.apply(this, my(arguments)))
    }}, I /* Using identity function as initial value. */ );
}

// Zero case works fine. This is kinda like a cat in unix. 
var I2 = pipe([]);

// add1 . add1 = add2 :)  Note the slight change in interface (using a list).
var add2 = pipe([add1, add1]);

// Yup works fine.
var add4 = pipe([add2, add2]);

console.log(add4(10)) // 14.

Upvotes: 0

salc2
salc2

Reputation: 577

Here is an alternative without for loop I think it is very clean and less verbose

function pipe(){
  var funs = arguments;
return function(){
    var args = arguments;
    function loop(fns,acc,index){
        if(index < fns.length ){
            return loop(fns,fns[index](acc),index+1);
        }else{
            return acc; 
        }
    }
    return loop(funs,funs[0].apply(this,args),1)
};
}

Evaluation

pipe(function(a,b,c){ return a+b+c;}, function(a){ return a+2; }, function(a){ return 3+a;})(1,2,3)

Upvotes: 0

jgawrych
jgawrych

Reputation: 3541

First, recognize that pipe can have an arbitrary number of arguments passed into it. To handle this we need to use the Arguments object or the Rest Parameter syntax to get the arbitrary number of arguments. Note that the rest parameter syntax is new, and not supported by all browsers, so it would be safer use the arguments object. Also note that arguments is array-like, but not an Array, so prototyped array methods cannot be directly called.

Next, recognize that the first function will be called with an arbitrary number of arguments. To handle this, we need to use the apply method that is on all functions.

Pipe will return a function, so that it can be later called.

Combining these ideas, we can construct pipe:

function pipe() {
    var fns = arguments;
    return function piping() {
        var val = fns[0].apply(this, arguments);
        for(var i = 1; i < fns.length; i++) {
            val = fns[i](val);
        }
        return val;
    }
}

And if we test it:

var foo = function (a, b, c) { return a + b + c; };
var bar = function (x) { return 2 * x; };
var baz = function (y) { return 1 + y; };
pipe(foo, bar, baz)(1, 2, 3);
// returns 13

We see that pipe returns 13, which is (1 + (2 * (a + b + c)))



Additionally, we may want bar and baz to take more than one argument. Changing the specification of pipe can achieve that goal. Adding an additional rule that all functions passed into pipe must return an array of arguments for the next function. This transforms pipe into a different result:

baz.apply(this, bar.apply(this, foo.apply(this, arguments)));

Lets redefine each function to take a number of arguments, and return a number of results as an array:

var foo = function (a, b, c) { return [a*a, b*b, c*c]; };
var bar = function (a, b, c) { return [a*2, b*2, c*2]; };
var baz = function (a, b, c) { return [a+1, b+1, c+1]; };
pipeArgsAsArrays(foo, bar, baz)(1, 2, 3);
// returns [3, 9, 19]

Reconstructing pipe to achieve this goal is simple:

function pipeArgsAsArrays() {
    var fns = arguments;
    return function piping() {
        var val = arguments;
        for(var i = 0; i < fns.length; i++) {
            val = fns[i].apply(this, val);
        }
        return val;
    }
}

We see that pipeArgsAsArrays returns [3, 9, 19], which is [a*a*2+1, b*b*2+1, c*c+2+1]

Upvotes: 3

Rainer Koirikivi
Rainer Koirikivi

Reputation: 751

One way would be to utilize a closure and the magic "arguments" object, that holds a list of arguments passed to a function:

function pipe() {                                                                                             
    var pipe_functions = arguments;
    return function(result) {
        for(var i = 0; i < pipe_functions.length; i++) {
            var func = pipe_functions[i];
            result = func(result);
        }
        return result;
    }
}

Then, for an example, if we have...

function foo(s) {
    return "foo" + s;
}
function bar(s) {
    return "bar" + s;
}
function baz(s) {
    return "baz" + s;
}

Calling the function like this:

pipe(foo, bar, baz)("lol");

returns "bazbarfoolol" which is exactly the same as

baz(bar(foo("lol")));

Upvotes: 1

xersiee
xersiee

Reputation: 4472

What about something like this?:

function pipe() {
  var functions = arguments;
  return function() {
    var result = arguments
    for (var i in functions) {
      result = functions[i](result);        
    }
    return result;
  }
}

Upvotes: 0

Related Questions