Currying in JavaScript
I've been thinking a lot lately about functional programming, and I thought it might be kind of fun to walk through the process of writing a curry
function.
For the uninitiated, currying refers to the process of taking a function with n
arguments and transforming it into n
functions that each take a single argument. It essentially creates a chain of partially applied functions that eventually resolves with a value.
Here's a basic example of how you'd use it:
function volume( l, w, h ) {
return l * w * h;
}
var curried = curry( volume );
curried( 1 )( 2 )( 3 ); // 6
Disclaimer
This post assumes basic familiarity with closures and higher-order functions, as well as stuff like Function#apply()
. If you're not comfortable with those concepts, you might want to brush up before reading further.
Writing our curry function
The first thing you'll notice is that curry
expects a function as its argument, so we'll start there.
function curry( fn ) {
}
Next, we need to know how many arguments our function expects (called its "arity"). Otherwise, we won't know when to stop returning new functions and give back a value instead.
We can tell how many arguments a function expects by accessing its length
property.
function curry( fn ) {
var arity = fn.length;
}
From there, things get a little bit trickier.
Essentially, every time a curried function is called, we add any new arguments to an array that's saved in a closure. If the number of arguments in that array is equal to the number of arguments that our original function expects, then we call it. Otherwise, we return a new function.
To do that, we need (1) a closure that can retain that list of arguments and (2) a function that can check the total number of arguments and either return another partially applied function or the return value of the original function with all of the arguments applied.
I usually do this with an immediately invoked function called resolver
.
function curry( fn ) {
var arity = fn.length;
return (function resolver() {
}());
}
Now, the first thing we need to do in resolver
is make a copy of any arguments it received. We'll do that by creating a variable called memory
that uses Array#slice
to make a copy of the arguments
object.
function curry( fn ) {
var arity = fn.length;
return (function resolver() {
var memory = Array.prototype.slice.call( arguments );
}());
}
Next, resolver
needs to return a function. This is what the outside world sees when it calls a curried function.
function curry( fn ) {
var arity = fn.length;
return (function resolver() {
var memory = Array.prototype.slice.call( arguments );
return function() {
};
}());
}
Since this internal function is the one that ends up actually being called, it needs to accept arguments. But it also needs to add those to any arguments that might be stored in memory
. So first, we'll make a copy of memory
by calling slice()
on it.
function curry( fn ) {
var arity = fn.length;
return (function resolver() {
var memory = Array.prototype.slice.call( arguments );
return function() {
var local = memory.slice();
};
}());
}
Now, lets add our new arguments by using Array#push
.
function curry( fn ) {
var arity = fn.length;
return (function resolver() {
var memory = Array.prototype.slice.call( arguments );
return function() {
var local = memory.slice();
Array.prototype.push.apply( local, arguments );
};
}());
}
Good. Now we have a new array containing all the arguments we've received so far in this chain of partially applied functions.
The last thing to do is to compare the length of arguments we've received with the arity of our curried function. If the lengths match, we'll call the original function. If not, we'll use resolver
to return yet another function that has all of our current arguments stored in memory.
function curry( fn ) {
var arity = fn.length;
return (function resolver() {
var memory = Array.prototype.slice.call( arguments );
return function() {
var local = memory.slice(), next;
Array.prototype.push.apply( local, arguments );
next = local.length >= arity ? fn : resolver;
return next.apply( null, local );
};
}());
}
This can be a little bit difficult to wrap your head around, so let's take it step by step in an example.
function volume( l, w, h ) {
return l * w * h;
}
var curried = curry( volume );
Okay, so curried
is the result of passing volume
into our curry
function.
If you look back, what's happening here is:
- We store the arity of
volume
, which is3
. - We immediately invoke
resolver
with no arguments, which means that for now, itsmemory
array is empty. resolver
returns an anonymous function.
Still with me? Now let's call our curried
function and pass in a length.
function volume( l, w, h ) {
return l * w * h;
}
var curried = curry( volume );
var length = curried( 2 );
Again, here are the steps:
- What we actually called here was the anonymous function being returned by
resolver
. - We made a copy of
memory
(which was empty) and called itlocal
. - We added our argument (
2
) to thelocal
array. - Since the length of
local
is less than the arity ofvolume
, we callresolver
again with the list of arguments we have so far. That creates a new closure with a newmemory
array, which contains our first argument of2
. - Finally,
resolver
returns a new function that has access to an outer closure with our newmemory
array.
So what we get back is that inner anonymous function again. But this time, it has access to a memory
array that isn't empty. It has our first argument (a 2
) inside of it.
If we call our length
function, the process repeats.
function volume( l, w, h ) {
return l * w * h;
}
var curried = curry( volume );
var length = curried( 2 );
var lengthAndWidth = length( 3 );
- Again, what we actually called was the anonymous function being returned by
resolver
. - This time,
resolver
had been primed with some previous arguments. So we make a copy of that array[ 2 ]
. - We add our new argument,
3
, to thelocal
array. - Since the length of
local
is still less than the arity ofvolume
, we callresolver
again with the list of arguments we have so far – and that returns a new function.
Now it's time to call our lengthAndWidth
function and get back a value.
function volume( l, w, h ) {
return l * w * h;
}
var curried = curry( volume );
var length = curried( 2 );
var lengthAndWidth = length( 3 );
console.log( lengthAndWidth( 4 ) ); // 24
This time, the steps are a little bit different at the end:
- Once again, what we actually called was the anonymous function being returned by
resolver
. - This time,
resolver
had been primed with two previous arguments. So we make a copy of that array[ 2, 3 ]
. - We add our new argument,
4
, to thelocal
array. - Now the length of
local
is3
, which is the arity ofvolume
. So instead of returning a new function, we return the result of callingvolume
with all of the arguments we've been saving up, and that gives us a value of24
.
Wrapping up
Admittedly, I have yet to find a super compelling use-case for currying in my day-to-day work. But I still think that going through the process of writing functions like this is a great way to improve your understanding of functional programming, and it helps reinforce concepts like closures and first-class functions.
By the way, if you like nerdy JavaScript things and live in the Boston area, I'm hiring at Project Decibel. Shoot me an email.