I wrote a simple curry
function in JavaScript which works correctly for most cases:
Apart from its mathematical definition
currying is the transformation of a function with
n
parameters into a sequence ofn
functions, which each accept a single parameter. The arity is thus transformed fromn-ary
ton * 1-ary
what impact has currying on programming? Abstraction over arity!
const comp = f => g => x => f(g(x));
const inc = x => x + 1;
const mul = y => x => x * y;
const sqr = x => mul(x)(x);
comp(sqr)(inc)(1); // 4
comp(mul)(inc)(1)(2); // 4
comp
expects two functions f
and g
and a single arbitrary argument x
. Consequently g
must be an unary function (a function with exactly one formal parameter) and f
too, since it is fed with the return value of g
. It won't surprise anyone that comp(sqr)(inc)(1)
works. sqr
and inc
are both unary.
But mul
is obviously a binary function. How on earth is that going to work? Because currying abstracted the arity of mul
. You can now probably imagine what a powerful feature currying is.
In ES2015 we can pre-curry our functions with arrow functions succinctly:
const map = (f, acc = []) => xs => xs.length > 0
? map(f, [...acc, f(xs[0])])(xs.slice(1))
: acc;
map(x => x + 1)([1,2,3]); // [2,3,4]
Nevertheless, we need a programmatic curry function for all functions out of our control. Since we learned that currying primarily means abstraction over arity, our implementation must not depend on Function.length
:
const curryN = (n, acc = []) => f => x => n > 1
? curryN(n - 1, [...acc, x])(f)
: f(...acc, x);
const map = (f, xs) => xs.map(x => f(x));
curryN(2)(map)(x => x + 1)([1,2,3]); // [2,3,4]
Passing the arity explicitly to curryN
has the nice side effect that we can curry variadic functions as well:
const sum = (...args) => args.reduce((acc, x) => acc + x, 0);
curryN(3)(sum)(1)(2)(3); // 6
One problem remains: Our curry solution can't deal with methods. OK, we can easily redefine methods that we need:
const concat = ys => xs => xs.concat(ys);
const append = x => concat([x]);
concat([4])([1,2,3]); // [1,2,3,4]
append([4])([1,2,3]); // [1,2,3,[4]]
An alternative is to adapt curryN
in a manner that it can handle both multi-argument functions and methods:
const curryN = (n, acc = []) => f => x => n > 1
? curryN(n - 1, [...acc, x])(f)
: typeof f === "function"
? f(...acc, x)
: x[f](...acc);
curryN(2)("concat")(4)([1,2,3]); // [1,2,3,4]
I don't know if this is the correct way to curry functions (and methods) in Javascript though. It is rather one possible way.
EDIT:
naomik pointed out that by using a default value the internal API of the curry function is partially exposed. The achieved simplification of the curry function comes thus at the expense of its stability. To avoid API leaking we need a wrapper function. We can utilize the U combinator (similar to naomik's solution with Y):
const U = f => f(f);
const curryN = U(h => acc => n => f => x => n > 1
? h(h)([...acc, x])(n-1)(f)
: f(...acc, x))([]);
Drawback: The implementation is harder to read and has a performance penalty.