Here's a 'compose' function which I need to improve:
const compose = (fns) => (...args) => fns.reduceRight((args, fn) => [fn(...args)], args)[0];
Here's a practical implementation of one:
const compose = (fns) => (...args) => fns.reduceRight((args, fn) => [fn(...args)], args)[0];
const fn = compose([
(x) => x - 8,
(x) => x ** 2,
(x, y) => (y > 0 ? x + 3 : x - 3),
]);
console.log(fn("3", 1)); // 1081
console.log(fn("3", -1)); // -8
And here's an improvement my mentor came to.
const compose = (fns) => (arg, ...restArgs) => fns.reduceRight((acc, func) => func(acc, ...restArgs), arg);
If we pass arguments list like that func(x, [y]) with first iteration, I still don't understand how do we make function work with unpacked array of [y]?
Let's analyse what the improved
composedoesWhen you feed
composewith a number of functions, you get back... a function. In your case you give it a name,fn.What does this
fnfunction look like? By simple substitution you can think of it as this:where
fns === [(x) => x - 8, (x) => x ** 2, (x, y) => (y > 0 ? x + 3 : x - 3)].So you can feed this function
fnwith some arguments, that will be "pattern-matched" against(arg, ...restArgs); in your example, when you callfn("3", 1),argis"3"andrestArgsis[1](so...restArgsexpands to just1after the comma, so you see thatfn("3", 1)reduces toFrom this you see that
(x, y) => (y > 0 ? x + 3 : x - 3)is called with the two arguments"3"(the initial value ofacc) and1,func,but the point is that the second argument to
func, namely1, is only used by the rightmost function, whereas it is passed to but ignored by the other two functions!Conclusion
Function composition is a thing between unary functions.¹ Using it with functions with higher-than-1 arity leads to confusion.²
For instance consider these two functions
can you compose them? Well, you can compose them into a function like this
the
composefunction that you've got at the bottom of your question would go well:But what if your two functions were these?
Your
composewould not work.Even though, if the function
pluswas curried, e.g. if it was defined asthen one could think of composing them in a function that acts like this:
which would predictably produce
f(3,4) === 10.You can get it as
f = compose([apply_twice, plus]).A cosmetic improvement
Additionally, I would suggest a "cosmetic" change: make
composeaccept...fnsinstead offns,and you'll be able to call it without groupint the functions to be composed in an array, e.g. you'd write
compose(apply_twice, plus)instead of.compose([apply_twice, plus])Btw, there's
lodashThere's two functions in that library that can handle function composition:
_.flow_.flowRight, which is aliased to_.composeinlodash/fp(¹) This is Haskell's choice (
.is the composition operator in Haskell). If you applyf . g . hto more than one argument, the first argument will be passed thought the whole pipeline; that intermediate result will be applied to the second argument; that further intermediate result will be applied to the third argument, and so on. In other words, if you hadhaskellComposein JavaScript, and iffwas binary andgandhunary,haskellCompose(f, g, h)(x, y)would be equal tof(g(h(x)), y).(²) Clojure's
compinstead takes another choice. It saturates the rightmost function and then passes the result over to the others. So if you hadclojureComposein JavaScript, andfandgwhere unary whilehbinary, thenclojureCompose(f, g, h)(x, y)would be equal tof(g(h(x,y))).Might be because I'm used to Haskell's automatically curryed functions, but I prefer Haskell's choice.