Let's analyse what the improved compose
does
compose = (fns) =>
(arg, ...restArgs) =>
fns.reduceRight((acc, func) => func(acc, ...restArgs), arg);
When you feed compose
with a number of functions, you get back... a function. In your case you give it a name, fn
.
What does this fn
function look like? By simple substitution you can think of it as this:
(arg, ...restArgs) => fns.reduceRight((acc, func) => func(acc, ...restArgs), arg);
where fns === [(x) => x - 8, (x) => x ** 2, (x, y) => (y > 0 ? x + 3 : x - 3)]
.
So you can feed this function fn
with some arguments, that will be "pattern-matched" against (arg, ...restArgs)
; in your example, when you call fn("3", 1)
, arg
is "3"
and restArgs
is [1]
(so ...restArgs
expands to just 1
after the comma, so you see that fn("3", 1)
reduces to
fns.reduceRight((acc, func) => func(acc, 1), "3");
From this you see that
- the rightmost function,
(x, y) => (y > 0 ? x + 3 : x - 3)
is called with the two arguments "3"
(the initial value of acc
) and 1
,
- the result will be passed as the first argument to the middle function with the following call to
func
,
- and so on,
but the point is that the second argument to func
, namely 1
, 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
square = (x) => x**2; // unary
plus = (x,y) => x + y; // binary
can you compose them? Well, you can compose them into a function like this
sum_and_square = (x,y) => square(plus(x,y));
the compose
function that you've got at the bottom of your question would go well:
sum_and_square = compose([square, plus]);
But what if your two functions were these?
apply_twice = (f) => ((x) => f(f(x))); // still unary, technically
plus = (x,y) => x + y; // still binary
Your compose
would not work.
Even though, if the function plus
was curried, e.g. if it was defined as
plus = (x) => (y) => x + y
then one could think of composing them in a function that acts like this:
f = (x,y) => apply_twice(plus(x))(y)
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 compose
accept ...fns
instead of fns
,
compose = (...fns)/* I've only added the three dots on this line */ =>
(arg, ...restArgs) =>
fns.reduceRight((acc, func) => func(acc, ...restArgs), arg);
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 lodash
There's two functions in that library that can handle function composition:
(¹) This is Haskell's choice (.
is the composition operator in Haskell). If you apply f . g . h
to 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 had haskellCompose
in JavaScript, and if f
was binary and g
and h
unary, haskellCompose(f, g, h)(x, y)
would be equal to f(g(h(x)), y)
.
(²) Clojure's comp
instead takes another choice. It saturates the rightmost function and then passes the result over to the others. So if you had clojureCompose
in JavaScript, and f
and g
where unary while h
binary, then clojureCompose(f, g, h)(x, y)
would be equal to f(g(h(x,y)))
.
Might be because I'm used to Haskell's automatically curryed functions, but I prefer Haskell's choice.