I tried to approach this by constraining the functions so that each function takes a single input and returns a single output. These functions are easy to compose, so it's better to think in terms of these functions.
How do we transform a normal, multi-parameter Javascript function into one that takes a single input and returns a single output? Currying of course!
Consider:
function f (a, b, c) {
return a + b + c;
}
function g (a) {
return function g1 (b) {
return function g2 (c) {
return a + b + c;
};
};
}
f(1, 2, 3) // => 6
g(1)(2)(3) // => 6
f is a normal function that returns the sum of its arguments. We can implement
f as a curried function by only accepting a single parameter and returning a function until we've finally received all the parameters. Once that happens, we can evaluate the result.
Since curried functions accept a single input and return a single output, the problem is reduced to "curry the functions, and then compose them just linke single-input-single-output functions".
Here's my naive implementation of curry(), it's based on
this informative article.
function curry (fn) {
function collectArgs (args, accumulatedArgs, numRemainingArgs) {
var acc = accumulatedArgs.concat(args);
if (0 >= numRemainingArgs) {
return fn.apply(this, acc);
}
return function () {
var args = Array.prototype.slice.call(arguments);
return collectArgs(args, acc, numRemainingArgs - arguments.length);
};
}
var numFnArgs = fn.length;
return collectArgs([], [], numFnArgs);
}
We rely on the fact that in Javascript, a
Function object has a
length property that tells us how many parameters it expects. We call
collectArgs() with that number and empty arrays for the arguments passed and the arguments accumulated, respectively.
collectArgs() will either evaluate the function if the correct number of arguments has been accumulated, or it will accumulate the arguments and return a function that, when called, will accumulate more arguments until the correct number is available.
We can use
curry() like this:
var f = curry(function (a, b, c) {
return a + b + c;
});
var add4 = f(2)(2); // partially apply f
add4(6); // => 10
Now, all we have to do is implement
compose() with the assumption that its arguments are curried functions. Note that we can be sure of this by currying the functions if they haven't been curried, I'll leave that as an exercise for you!
function compose (f, g) {
return function () {
var r = g.apply(this, arguments),
rs = [r];
return f.apply(this, rs);
};
}
This is a straightforward translation of the
compose() from the example. We return a function that first applies
g() to its arguments, and then applies
f() to the result. Note that since they're curried,
f() and
g() can accept as many arguments as they want, and the composition will still work.
For example:
var addstuff = function (x, y) {
return x + y;
};
var mulstuff = function (x, y) {
return x * y;
};
var dostuff = compose(addstuff, mulstuff);
dostuff(2, 3)(7); // => 13
In this example,
dostuff() passes its first two parameters to
mulstuff(), which multiplies them to produce
6. It then passes that result to
addstuff(). Since
addstuff() expects two parameters, and it got only one, the result is a function expecting the remaining parameter. This is because
addstuff() is a curried function, and we've partially applied it to one parameter. When we pass it the next parameter,
7, it evaluates the result of adding that to
6, which is
13.
You can think of this as a rough draft for a more general solution. The syntax can be improved with some more tinkering, and weird conditions like partially applying the inner composed function still need to be handled. Additionally, I haven't considered composing an unknown number of functions, but I know it's possible by generalizing the simple composition.