As a Software Engineer, it might sound weird to confess that I only recently learned about currying while watching a course about functional JavaScript fundamentals. I was familiar with most subjects covered in the course (i.e. closure actions and recursive functions), but currying was new territory for me. The basics of currying from that course sparked my interest in the possibilities and complexity behind it.
To make sure I would understand the whole concept, I played around and wrote my own currying functions. I started with a basic one to gain an understanding of currying and ended up with a more advanced version with placeholders after refactoring and improving.
This piece will take you with me on a simplified trip of that journey. All code examples can be found here as well.
What is Currying?
Currying is the concept of transforming a function with multiple arguments into a sequence of functions each taking one (or several) argument(s) of the original function. This way you can partially apply arguments to the function and it will only execute until it has received all of the expected arguments.
Let’s take a look at this example:
const createChallenge = (name, opponent, game) => {
console.log(`${name}, is challenging ${opponent} to play a game of ${game}!`);
};
And its curried function:
const curriedChallenge = name => {
return opponent => {
return game => {
// console.log(`${name} is challenging ${opponent} to play a game of ${game}!`);
createChallenge(name, opponent, game);
};
};
};
Currying makes use of the concept of closure functions, where a function returns another function that still has access to its parent function’s scope.
Instead of passing every argument at the same time, you can now split them out into multiple function calls.
createChallenge('John', 'Sam', 'chess');
curriedChallenge('John')('Sam')('chess');
This makes it easier to set arguments as soon as you know of them because the curried function will wait, instead of having to do it yourself. Currying can also be helpful when doing things multiple times with just slightly different data:
const JohnChallenges = curriedChallenge('John');
JohnChallenges('Frank')('chess');
JohnChallenges('Paul')('tic tac toe');
const JohnChallengesMark = JohnChallenges('Mark');
JohnChallengesMark('chess');
JohnChallengesMark('tic tac toe');
JohnChallengesMark('scrabble');
Build Our Own Curry Transformer
There are many libraries you can use to convert your function to a curried
function, like Lodash (_.curry(yourFunction)
). By the time I understood
the concept of currying, I was curious to build my own.
For this we have to think in a more abstract way.
const originalFunction = (a, b, c) => {
// do something with a, b and c
};
const curriedFunction = a => {
return b => {
return c => {
originalFunction(a, b, c);
};
};
};
At first, I struggled with function scopes and keeping track of all the
passed arguments for each function. Finally, I found the best way to do this
is by creating a recursive function to wait for the expected amount of arguments.
Once you reach the right amount of arguments, you can trigger the original
function with all the arguments, otherwise you’ll keep waiting for more arguments and
pass them to the next nested function call. With .length
on the original
function, we can determine how many arguments we have to wait for before
executing the original function.
const curry = (functionToCurry) => {
const waitForArguments = (...attrs) => {
return attrs.length === functionToCurry.length ?
functionToCurry(...attrs) :
(...nextAttrs) => waitForArguments(...attrs, ...nextAttrs);
};
return waitForArguments;
};
We can now use this function to convert our own function to its curried version. Remember how we had to pass one argument at a time in our previous version? With this version, we can decide how many arguments we will pass each time since the returned function grabs all arguments instead of just one. This is where it is a real curried function.
const curriedChallenge = curry(createChallenge);
curriedChallenge('John', 'Sam', 'chess');
curriedChallenge('John')('Sam')('chess');
curriedChallenge('John', 'Sam')('chess');
curriedChallenge('John')('Sam', 'chess');
Arity
Note that currying this way will only work for functions with a fixed number of arguments. We could add an optional argument to our curry function with the arity (expected number of arguments) of the passed function.
const curry = (functionToCurry, arity = functionToCurry.length) => {
const waitForArguments = (...attrs) => {
return attrs.length >= arity ?
functionToCurry(...attrs) :
(...nextAttrs) => waitForArguments(...attrs, ...nextAttrs);
};
return waitForArguments;
};
This will not only make it possible to curry functions without a fixed amount of arguments, it will also extend the possibilities to create functions with an unknown amount of arguments.
Currying a function with an indefinite amount of arguments can make it possible to use the generic logic of your function and add an extra layer of logic / expectations to it. This might not be the best example, but something I came up with to explain this is an imaginary game/assignment where you have to combine multiple numbers to come to a predefined total. It can be defined as a generic sum function, and you can set the amount of needed numbers later. It won’t be checking until you’ve passed the right amount of numbers.
const sumTo100 = (...args) => {
const sum = args.reduce((total, number) => total + number);
console.log(sum === 100);
};
const sumTo100In2Parts = curry(sumTo100, 2);
sumTo100In2Parts(20, 80); // true
sumTo100In2Parts(40)(60); // true
const sumTo100In5Parts = curry(sumTo100, 5);
sumTo100In5Parts(10, 20, 30, 10, 30); // true
const firstThree = sumTo100In5Parts(10, 20, 30); // function waiting for arguments
firstThree(10, 30); // true
Currying with Placeholders
In addition to how our curried function works at this point, many libraries are
covering placeholder (_
) arguments as well. This makes it possible to change the
order in which you have to pass the arguments by skipping things you don’t know
yet. Which means you could do things like:
const playChess = curriedChallenge(_, _, 'chess');
playChess('John', 'Sam');
playChess('Mark', 'Mary');
We can skip attributes and pass them later in the same order to fullfill all expected arguments. Let’s take a look at how we could accomplish that in our code.
// Create unique identifier to use as a placeholder. This could be named anything.
const _ = Symbol();
const curry = (functionToCurry, numberOfArguments = functionToCurry.length) => {
const waitForArguments = (...attrs) => {
const waitForMoreArguments = (...nextAttrs) => {
const filledAttrs = attrs.map(attr => {
// if any of attrs is placeholder _, nextAttrs should first fill that
return attr === _ && nextAttrs.length ?
nextAttrs.shift() :
attr;
});
return waitForArguments(...filledAttrs, ...nextAttrs);
};
// wait for all arguments to be present and not skipped
return attrs.filter(arg => arg !== _).length >= numberOfArguments ?
functionToCurry(...attrs) :
waitForMoreArguments;
};
return waitForArguments;
};
It only took me a couple changes to make this work. 1) It shouldn’t call the initial function until all the arguments are present and 2) it must replace previous given placeholders with newly passed arguments in the same order.
Note that I had to create an identifier to use as a placeholder. This could be
anything, but _
is a very common thing to use for placeholders or unused
attributes, so I used that.
Wrapping Up
Following these steps to create my own currying transformer helped me to
understand the concept of currying. It made me think in more complex ways to
create easy generic functions and make them more useful by adding additional
logic like currying. Currying might become more important, as it relates to
function composition, if the |>
operator makes its way through the JS standards.
But, more about that in a future blog!
DockYard is a digital product agency offering custom software, mobile, and web application development consulting. We provide exceptional professional services in strategy, user experience, design, and full stack engineering using Ember.js, React.js, Ruby, and Elixir. With a nationwide staff, we’ve got consultants in key markets across the United States, including San Francisco, Los Angeles, Denver, Chicago, Austin, New York, and Boston.