Reputation: 31
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
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
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
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
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
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
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