Introduction to currying in JavaScript
Implementations and practical examples
Introduction
We want to specialize a JavaScript function of fixed arity by transforming it first into a composition of functions of lower arities.
First, consider a function of the following form:
const fn = (p1, p2, ..., pN) => f(p1, p2, ..., pN);
In its simplest form, this problem consists of transforming this function into a new function of the following form:
const curried = p1 => (p2, ..., pN) => fn(p1, p2, ..., pN);
We can also generalize this process to get the following form:
const curried = p1 => p2 => ... => pN => fn(p1, p2, ..., pN);
This last transformation is also called currying after the American mathematician Haskell Curry whose work laid the foundations of functional programming.
In this article, we propose to study different aspects of currying in JavaScript. In particular, we will see:
- a simple example to understand what currying is in practice.
- several JavaScript implementations allowing to carry out the transformations above.
- a generic and flexible variant of transforming a JavaScript function into a composition of functions of variable arity.
- an example illustrating the limits of the proposed implementations with regard to the currying of functions of variable arity.
A simple example of currying
We want to compute the price all taxes included (ATI) of products belonging to different categories using a function ATI
:
const ATI = (rate, price) => (1 + rate) * price;const price_tv = ATI(0.2, 299);
const price_apples = ATI(0.055, 1.99);
const price_tomatoes = ATI(0.055, 2.99);
Now let’s rewrite our function ATI
in its curried form:
const ATI = rate => price => (1 + rate) * price;
This allows us in particular to define functions for computing the price all taxes included by category:
const ATI_service = ATI(0.2);
const ATI_first_necessity = ATI(0.055);
We can thus rewrite our price computations including taxes without repeating the value of the used rate:
const ATI_tv = ATI_service(299);
const ATI_apples = ATI_first_necessity(1.99);
const ATI_tomatoes = ATI_first_necessity(2.99);
This is a very simple example of using currying so that we can then specialize a function by setting its first parameter.
Although this is not the subject of this article, note that we can also rewrite our functions ATI_service
andATI_first_necessity
as follows:
const ATI_service = price => ATI(0.2, price);
const ATI_first_necessity = price => ATI(0.055, price);
The resulting functions are called partial applications. We will also say that we have projected the ATI
function on the rest of the parameters which have not been fixed. The advantage of this technique is to be able to set certain parameters and return a new function of lower arity.
Even if, as the example above shows, the projection of the ATI
function on theprice
parameter gives a function similar to the curried function, one must not confuse currying and partial application:
- currying makes it possible to transform a function with several parameters into a composition of unary functions.
- a partial application allows to fix a certain number of parameters of a function and returns a function of lower arity.
In this article, we will implement a generic currying function allowing the currying process to be carried out automatically, regardless of the number of parameters of the considered function.
We would like to be able to obtain, for example, the second ATI
function from the first as follows:
const ATI_curried = curry(ATI);
Currying implementations
Simple currying
Consider a function of the following form:
const fn = (p1, p2, ..., pN) => f(p1, p2, ..., pN);
We want to transform it into a new function of the following form:
const curried = p1 => (p2, ..., pN) => fn(p1, p2, ..., pN);
Solution
Here is a possible solution:
const curryOne = fn => first => (...args) => fn(first, ...args);
Here, the curryOne
function takes a function fn
as a parameter and returns a new function taking a parameter first
. When we call the unary function thus produced, we obtain a new function having memorized the first argument passed to the parameter first
, and applicable to the rest of the parameters of the function fn
.
A more concise solution
Note that the curryOne
function can be rewritten in the following more concise way:
const curryOne = fn => first => fn.bind(null, first);
Indeed, the Function.prototype.bind()
method returns here a new function with a bound argument which will be automatically placed before the other arguments which will be passed to it, which is very convenient to achieve the expected transformation.
General currying
Now consider the general form of the curried function we want to achieve:
const curried = p1 => p2 => ... => pN => fn(p1, ..., pN);
Before tackling the implementation, let’s revisit our function curryOne
:
const curryOne = fn => first => (...args) => fn(first, ...args);
And then let’s try to go a little further by applying a new simple currying transformation to the first function that is returned:
const curryTwo = fn =
first =>
second =>
(...args) =>
fn(first, second, ...args);
To be able to generalize this solution, we must first observe that in all cases, it is the last function that is responsible for calling the initial function fn
on all the arguments. In addition, the arguments encountered during the course of the successive unary functions calls are stacked from left to right in the final call: a mechanism must therefore be found so that this function can store in the correct order all the arguments passed successively to the unary functions.
Solution
Here is a possible solution corresponding to our previous observation:
const curry = fn => {
const curried =
(...acc) =>
acc.length === fn.length ?
fn(...acc) :
p => curried(...acc, p); return curried();
};
We build here a curried function curried
using an ascending recursion corresponding to the accumulation inacc
of the arguments passed successively to the unary functions:
- Terminal case: all the arguments have been consumed and the length of the accumulator of arguments is equal to the arity
fn.length
of the function. - If all the arguments have not yet been consumed, then we return a unary function
p => curried (... acc, p)
. In particular, we observe that at each call ofcurried
, the length of the accumulator is incremented by one and our recursion ends when its length reaches the arity of the functionfn
.
A more concise solution
Note that in the same way as in the previous section, we can write a more concise function using the Function.prototype.bind()
method which naturally accumulates the arguments on the left.
const curry = fn =>
fn.length === 0 ?
fn() :
p => curry(fn.bind(null, p));
Flexible currying
We may need a little more flexibility when currying a function.
Say we want to be able to specialize a function by specifying multiple arguments at once instead of making multiple function calls:
const sum = curry((a, b, c) => a + b + c);sum(1)(2)(3); // => 6
sum(1, 2)(3); // => 6
sum(1, 2, 3); // => 6
Solution
We can simply take the general currying function seen previously and replace the unary functions with functions of variable arity:
const curry = fn => {
const curried =
(...acc) =>
acc.length === fn.length ?
fn(...acc) :
(...args) => curried(...acc, ...args); return curried();
};
Also, we can simplify this solution. This is because we were using an intermediate constant curried
to make sure that we return a unary function, which is what the call to the function curried
does. Now, since our curry function can take any number of arguments as parameters, we can rewrite the function curry
without any intermediate function:
const curry = fn =>
(...acc) =>
acc.length === fn.length ?
fn(...acc) :
(...args) => curry(fn)(...acc, ...args);
A more concise solution
The use of the Function.prototype.bind()
method allows us to write the previous function more concisely, by implicitly accumulating the arguments in the function returned on each recursive call:
const curry = fn =>
fn.length === 0 ?
fn() :
(...args) => curry(fn.bind(null, ...args));
Limitations of the proposed implementations
Here is an example of a nifty generic function that lets us compute a sum of numbers in several ways:
const sum = (...args) =>
Object.assign(
sum.bind(null, ...args),
{valueOf: () => args.reduce((acc, cur) => acc + cur, 0)}
);+sum(1)(2)(3); // => 6
+sum(1, 2)(3); // => 6
+sum(1, 2, 3); // => 6
Now let’s try to reuse the generic curry
function we wrote earlier to make this code more readable:
const add = (a, b) => a + b;
const sum = curry((...args) => args.reduce(add, 0));
sum(1)(2)(3); // TypeError: sum is not a function
Oops! As you probably expected, the line calling curry
does not produce the correct result: this is because for a function fn
accepting a variable number of arguments, the value of fn.length
which defines the terminal case of our recursion is… 0
!
Thus, we fall directly into the terminal case of our recursive function and our attempt at currying is equivalent to:
const sum = ((...args) => args.reduce(add, 0))(); // sum = 0
This is of course not what we are trying to do, but this problem allows us to stress that our currying function only applies to a function with a fixed arity.
In case you still need to curry functions of variable arities, you can always modify the code of the function curry
to take into account an additional parameter which would indicate the expected number of parameters: our function must know when to stop and return a value.
That being said, we can generalize our function curry
in some cases. For example, by generalizing the sum
function seen at the beginning of this section, we can write:
const add = (a, b) => a + b;const curryWithBinaryOp = (op, initialValue) => {
const reduced = (...args) =>
Object.assign(
reduced.bind(null, ...args),
{ valueOf: () => args.reduce(op, initialValue) }
); return reduced;
};const sum = curryWithBinaryOp(add, 0);+sum(1)(2)(3); // => 6
+sum(1, 2)(3); // => 6
+sum(1, 2, 3); // => 6
This is a clever way of currying functions of variable arity. Note, however, that it is necessary to tell JavaScript that we want to extract the resulting value (by casting the returned function by prefixing it with +
) since our curried function always returns a function when called and does not know in advance where we are going to stop! So the trick of using the valueOf
property allows us to extract the value that interests us when we need it.
I originally wrote this article in French and you can find the original version here: