The current prevailing approach provides that each multi argument function is wrapped in a dynamic curry function. While this helps with concern #1, it leaves the remaining ones untouched. Here is an alternative approach.
Composable functions
A composable function is curried only in its last argument. To distinguish them from normal multi argument functions, I name them with a trailing underscore (naming is hard).
const comp_ = (f, g) => x => f(g(x)); // composable function
const foldl_ = (f, acc) => xs => xs.reduce((acc, x, i) => f(acc, x, i), acc);
const curry = f => y => x => f(x, y); // fully curried function
const drop = (xs, n) => xs.slice(n); // normal, multi argument function
const add = (x, y) => x + y;
const sum = foldl_(add, 0);
const dropAndSum = comp_(sum, curry(drop) (1));
console.log(
dropAndSum([1,2,3,4]) // 9
);
With the exception of drop
, dropAndSum
consists exclusively of multi argument or composable functions and yet we've achieved the same expressiveness as with fully curried functions - at least with this example.
You can see that each composable function expects either uncurried or other composable functions as arguments. This will increase speed especially for iterative function applications. However, this is also restrictive as soon as the result of a composable function is a function again. Look into the countWhere
example below for more information.
Programmatic solution
Instead of defining composable functions manually we can easily implement a programmatic solution:
// generic functions
const composable = f => (...args) => x => f(...args, x);
const foldr = (f, acc, xs) =>
xs.reduceRight((acc, x, i) => f(x, acc, i), acc);
const comp_ = (f, g) => x => f(g(x));
const I = x => x;
const inc = x => x + 1;
// derived functions
const foldr_ = composable(foldr);
const compn_ = foldr_(comp_, I);
const inc3 = compn_([inc, inc, inc]);
// and run...
console.log(
inc3(0) // 3
);
Operator functions vs. higher order functions
Maybe you noticed that curry
(form the first example) swaps arguments, while composable
does not. curry
is meant to be applied to operator functions like drop
or sub
only, which have a different argument order in curried and uncurried form respectively. An operator function is any function that expects only non-functional arguments. In this sence...
const I = x => x;
const eq = (x, y) => x === y; // are operator functions
// whereas
const A = (f, x) => f(x);
const U = f => f(f); // are not operator but a higher order functions
Higher order functions (HOFs) don't need swapped arguments but you will regularly encounter them with arities higher than two, hence the composbale
function is useful.
HOFs are one of the most awesome tools in functional programming. They abstract from function application. This is the reason why we use them all the time.
A more serious task
We can solve more complex tasks as well:
// generic functions
const composable = f => (...args) => x => f(...args, x);
const filter = (f, xs) => xs.filter(f);
const comp2 = (f, g, x, y) => f(g(x, y));
const len = xs => xs.length;
const odd = x => x % 2 === 1;
// compositions
const countWhere_ = f => composable(comp2) (len, filter, f); // (A)
const countWhereOdd = countWhere_(odd);
// and run...
console.log(
countWhereOdd([1,2,3,4,5]) // 3
);
Please note that in line A
we were forced to pass f
explicitly. This is one of the drawbacks of composable against curried functions: Sometimes we need to pass the data explicitly. However, if you dislike point-free style, this is actually an advantage.
Conclusion
Making functions composable mitigates the following concerns:
- aesthetic concerns (less frequent use of the curry pattern
f(x) (y) (z)
- performance penalties (far fewer function calls)
However, point #4 (readability) is only slightly improved (less point-free style) and point #3 (debugging) not at all.
While I am convinced that a fully curried approach is superior to the one presented here, I think composable higher order functions are worth thinking about. Just use them as long as you or your coworkers don't feel comfortable with proper currying.