Let's build a functional programming mainstay: the curry function. We will work through identifying, writing, troubleshooting, and improving a curry function.
Getting Started
Code Examples
Most code examples can be run in isolation, though some expect definitions from a previous block to be available.
I use the pattern of a comment following the line to represent the output of that code.
// Example of operation and output
2 + 2;
// 4
If you would like to follow along or try the code examples for yourself, I strongly recommend using RunJS.
What is Currying
Currying is an important part of functional programming, and a great example of higher-order functions.
Broadly speaking, currying creates a collector or accumulator of function arguments. A curried function returns another accumulator until it has all of the arguments needed to execute the function. Currying is a specific form of partial application.
Here is a simple example of the difference between regular and curried function calls.
// Regular example
let addThree = (a, b, c) => a + b + c;
addThree(1, 2, 3);
// Now, curried! Just an example, no curry function yet.
addThree = curry((a, b, c) => a + b + c);
addThree(1)(2)(3);
addThree(1, 2)(3);
addThree(1)(2, 3);
addThree(1, 2, 3);
Some strict curry implementations may allow only one argument to be passed at a time. This can be useful, and we will discuss it later, but we aren't going to hold to that limitation.
Breaking it Down
What happens when we curry and then execute a function roughly works out to several steps:
- We pass a function to
curry
. - Curry counts the number of arguments the function expects.
- Curry returns an accumulator function.
- We call the accumulator with some or all of the arguments.
- Curry returns an accumulator until all of the expected arguments of the original function are provided.
- When all the arguments are provided, curry executes the original function with the arguments.
Now that we have steps, we can try to create this behavior for ourselves. We will build some simple versions and improve upon them, trying to explain out thoughts and limitations along the way.
Getting Started: Manual Currying
First, let's build the most basic form: manual currying. Just as a proof-of-concept, we're going to make a function that adds two numbers, and then a curried version.
// Almost as simple as it gets
const add = (a, b) => a + b;
add(1, 2);
// 3
// Manually curried with two arrow functions
const curriedAdd = (a) => (b) => a + b;
add(1)(2);
// 3
If you are not used to working with higher-order functions, that curried version may be difficult to read, so let's talk it through.
curriedAdd
is a function which takes the argumenta
and returns a second function. The second function takes the argumentb
and returnsa
plusb
.
The second function still has access to the first argument thanks to closures, so we can access both to complete the work.
If you are still unsure about the nested functions, here are a few more ways to write it that may help.
// With extra parentheses
const curriedAdd2 = (a) => ((b) => a + b);
// With braces
const curriedAdd3 = (a) => {
return (b) => a + b;
};
// With old-school functions
const curriedAdd4 = function (a) {
return function (b) {
return a + b;
};
};
Hopefully one or more of the examples helped clarify the function-in-function behavior.
Real Life Currying
The examples above don't show how we would actually use currying. Let's take a look at why it might be useful.
Currying lets us split ownership of different arguments to make reusable functions.
// Some generic data
const distances = [ 1, 2, 4, 5, 8 ];
// Our curried multiply function
const multiply = curry((a, b) => a * b);
// Find the first value somewhere.
const factor = getConversion('mile', 'km');
// 1.6
const convertMileToKilometer = multiply(factor);
const newDistances = distances.map(convertMileToKilometer);
// [ 1.6, 3.2, 6.4, 8, 12.8 ]
We can also see this being useful if we have to fetch parameters asynchronously.
// Some generic data
const distances = [ 1, 2, 4, 5, 8 ];
// Our curried add function
const multiply = curry((a, b) => a * b);
fetchConversionFactor('mile', 'km')
.then((factor) => multiply(factor))
.then(converter => distances.map(converter));
// Just for giggles, we can avoid the extra arrow functions
fetchConversionFactor('mile', 'km')
.then(multiply)
.then(distances.map.bind(distances));
When values or operations come from different places, currying can allow different "owners" to contribute to code without the end consumer needing to know about the steps along the way.
Arity
Before we dive in, we need to learn a new term: arity.
Arity is the number of arguments a function accepts. This can be found by accessing the .length
property on a function.
There are some limitations because length
does not include rest parameters and stops at the first default value. Here are some quick examples:
const example1 = (a, b) => a + b;
example1;
// 2
const example2 = (a, b = 1) => a + b;
example2.length;
// 1 - Stop at the default value
const example3 = (a, b = 1, c) => a + b + c;
example3.length;
// 1 - Stop at the default value
const example4 = (a, b, ...c) => a + b + sum(c);
example4.length;
// 2 - Do not include rest parameters
const example5 = (...a) => sum(a);
example5.length;
// 0 - No regular parameters
Arity is important to making a proper curry function because we need to know when to stop accepting arguments and run the curried function.
Starting a Curry
Back in the What is Currying section, we identified several step that happen when currying. Let's see those again.
- We pass a function to
curry
. - Curry counts the number of arguments the function expects.
- Curry returns an accumulator function.
- We call the accumulator with some or all of the arguments.
- Curry returns an accumulator until all of the expected arguments of the original function are provided.
- When all the arguments are provided curry executes the original function with the arguments.
We can use a simple currying example to test our progress:
const add = (a, b) => a + b;
const curriedAdd = curry(add);
curriedAdd(1)(2);
1. We pass a function to curry
.
const curry = (fn) => fn;
// Test it!
const add = (a, b) => a + b;
const curriedAdd = curry(add);
curriedAdd(1)(2);
// TypeError: curriedAdd(...) is not a function
Not so good, yet. The second function call doesn't work.
2. Curry counts the number of arguments the function expects.
const curry = (fn) => {
const arity = fn.length;
return fn;
};
// Test it!
const add = (a, b) => a + b;
const curriedAdd = curry(add);
curriedAdd(1)(2);
// TypeError: curriedAdd(...) is not a function
We counted, but didn't make any other change, so we need more.
3. Curry returns an accumulator function.
const curry = (fn) => {
const arity = fn.length;
const previousArgs = [];
const accumulator = (arg) => {
// Keep track of the arguments.
previousArgs.push(arg);
};
return accumulator;
};
// Test it!
const add = (a, b) => a + b;
const curriedAdd = curry(add);
curriedAdd(1)(2);
// TypeError: curriedAdd(...) is not a function
We're building up, but now we don't actually run the function, and we still have a type error for that second call.
4. We call the accumulator with some or all of the arguments.
This is actually already covered by our tests in the previous step, so no new code for this one.
5. Curry returns an accumulator until all of the expected arguments of the original function are provided.
const curry = (fn) => {
const arity = fn.length;
const previousArgs = [];
const accumulator = (arg) => {
previousArgs.push(arg);
if (previousArgs.length < arity) return accumulator;
};
return accumulator;
};
// Test it!
const add = (a, b) => a + b;
const curriedAdd = curry(add);
curriedAdd(1)(2);
// undefined
Well, we aren't throwing an error anymore, but we aren't running the original function, either. Our code should probably do that at some point...
6. When all the arguments are provided curry executes the original function with the arguments.
That's what we needed!
const curry = (fn) => {
const arity = fn.length;
const previousArgs = [];
const accumulator = (arg) => {
previousArgs.push(arg);
if (previousArgs.length < arity) return accumulator;
// Run the function when we have enough arguments
return fn(...previousArgs);
};
return accumulator;
};
// Test it!
const add = (a, b) => a + b;
const curriedAdd = curry(add);
curriedAdd(1)(2);
// 3
We did it!
Testing Capabilities
We've done a simple two-argument curry, but there are some limitations to our design. Let's revisit the addThree
example at the beginning and see how we do:
addThree = curry((a, b, c) => a + b + c);
addThree(1)(2)(3);
// 6
addThree(7,8,9);
// 6
addThree(1, 2)(3);
// TypeError: addThree(...) is not a function
I'm pretty sure 7 + 8 + 9
is not six, and we get errors when we try to pass arguments in more than one call, so we need to figure out what happened.
Don't Share Closures
It turns out we made a terrible mistake! We kept a single list of arguments in an outer closure, so we share a closure for all uses of the function. This means we can only use it one time, and if we tried to pass partial arguments to it several times, it would behave incorrectly.
const add = curry((a, b) => a + b);
// Try to make two different functions
const addTwo = add(2);
// ƒ accumulator()
// This doesn't return a function because of the shared accumulator.
const addThree = add(3);
// 5
This is sort of the opposite of currying. Looking back at What is Currying section, there is some key language that describes the problem:
A curried function returns another accumulator until it has all of the arguments...
Each step of the currying process needs to have a unique set of arguments, so each step creates a reusable function. To do this, we'll need to pass our collection of arguments to each successive accumulator.
We could go about this a couple of ways... we could use Function.prototype.bind()
to set the first value, or we could use an another arrow function to provide a closure. I'm going with the arrow function to avoid setting a context.
const curry = (fn) => {
const arity = fn.length;
const accumulator = (previousArgs) => (arg) => {
const newArgs = [...previousArgs, arg];
if (newArgs.length < arity) return accumulator(newArgs);
// Run the function when we have enough arguments
return fn(...newArgs);
};
// Start with no arguments passed.
return accumulator([]);
};
Now, each time we call the accumulator we first pass in the previous arguments and get back a function that takes the next argument. You can see our first call to accumulator passes an empty array. If we didn't do this, it would cause an error when spreading previousArgs
. We could also have used a default parameter, if we wanted.
Let's see how this version works.
addThree(1)(2)(3); // 6
addThree(1, 2)(3); // ƒ ()
addThree(1)(2, 3); // ƒ ()
addThree(1, 2, 3); // ƒ ()
No errors anymore, but we aren't successfully processing versions that pass more than one argument at a time. But that's an easy fix. We need to take ...args
instead of arg
.
Accept Multiple Arguments
I mentioned before that strict versions of curry accept only one argument at a time. This can be useful when you need to use the function in a place where you don't want all the arguments passed to it, like in Array.prototype.map()
. Because the mapper receives three arguments – value, index, and array – it would misbehave if curry accepts multiple values.
const multipliers = [2, 4, 6, 8];
const multiply = curry((a, b) => a * b);
const multiplierFns = multipliers.map(multiply);
// With one argument at a time:
// [ ƒ (), ƒ (), ƒ (), ƒ () ]
// Supporting multiple arguments:
// [ 0, 4, 12, 24 ]
While this might seem like we should only accept one argument at a time, our code is more flexible for all the other times. The Array prototype function issue is well-known, and a common example is seeing .map(parseInt)
misbehave. For situations where we know we have a concern, like .map()
, we can create a simple unary function wrapper to prevent the error.
const multipliers = [2, 4, 6, 8];
const multiply = curry((a, b) => a * b);
// Only one argument, no matter how curry is written
const multiplierFns = multipliers.map((value) => multiply(value));
// [ ƒ (), ƒ (), ƒ (), ƒ () ]
It's a pretty good pattern. It allows curry
to be flexible and you put the fix for "bad arguments" in the place where they happen. This is common enough that libraries like Lodash include a unary function.
So, let's not worry about those cases, and move forward with ...args
.
const curry = (fn) => {
const arity = fn.length;
// Accept more than one argument at a time!
const accumulator = (previousArgs) => (...args) => {
const newArgs = [...previousArgs, ...args];
if (newArgs.length < arity) return accumulator(newArgs);
// Run the function when we have enough arguments
return fn(...newArgs);
};
// Start with no arguments passed.
return accumulator([]);
};
And some quick testing:
addThree(1)(2)(3); // 6
addThree(1, 2)(3); // 6
addThree(1)(2, 3); // 6
addThree(1, 2, 3); // 6
Now our various argument styles seem to be working as expected. To be sure, we should test the reusability of our functions, which was a problem with our early closure design.
const add = (a,b) => a + b;
const curriedAdd = curry(add);
const addTwo = curriedAdd(2);
addTwo(10); // 12
addTwo(5); // 7
addTwo(-2); // 0
Looking good!
Setting Arity
Right now our curry function depends on the length
property of the function, but as we mentioned when we learned about arity, there are reasons it can be "wrong", or at least different from what we need.
const addAll = (...args) => args.reduce((a, b) => a + b, 0);
addAll.length; // 0
const multiply = (a, b = -1) => a * b;
multiply.length; // 1
For cases like these, we need to set the arity for currying to work correctly. It's easy enough to add an optional second argument to the curry function, and we can leverage how default parameters have access to the earlier parameters to add this with less code than before by moving arity
into the arguments.
const curry = (fn, arity = fn.length) => {
// Accept more than one argument at a time!
const accumulator = (previousArgs) => (...args) => {
const newArgs = [...previousArgs, ...args];
if (newArgs.length < arity) return accumulator(newArgs);
// Run the function when we have enough arguments
return fn(...newArgs);
};
// Start with no arguments passed.
return accumulator([]);
};
const add = (...args) => args.reduce((a, b) => a + b, 0);
curry(add, 4)(1)(2)(3)(4); // 10
curry(add, 2)(10, 5); // 15
Other Considerations?
Context
Context can be a complex and annoying part of JavaScript, but we might need to add the ability to accept a context. We could pass the context at curry time, but if we know the context at that point, we can just pass a bound function to curry in the first place. Bind passes along the function length, so we don't have to make any changes to our existing curry function to support that.
curry(doSomethingWithContext.bind(this));
If we need a dynamic context, it will be the context called last. For this to work we cannot use all arrow functions, as they use the context in which they were declared. Only the one function that actually receives new arguments needs to be changed, though.
const curry = (fn, arity = fn.length) => {
// Accept more than one argument at a time!
const accumulator = (previousArgs) => function (...args) {
const newArgs = [...previousArgs, ...args];
if (newArgs.length < arity) return accumulator(newArgs);
// Run with context when we have enough arguments
return fn.apply(this, newArgs);
};
// Start with no arguments passed.
return accumulator([]);
};
Now, a very contrived example for testing.
const makeMessage = function(quantity, color) {
return `Find ${quantity} ${color} ${this.noun}!`;
};
const requestColor = curry(makeMessage)(4);
const myContext = {
noun: 'airplanes',
requestColor,
};
myContext.requestColor('blue'); // "Find 4 blue airplanes!"
I included this in "Other Considerations" because it blends in parts of Object-Oriented Programming that we mostly try to avoid in Functional Programming. Adding support for context is simple enough and can enable the use of curried functionality inside OOP, but I try to avoid blending them together this way when I can help it.
Bad Types
Our curry doesn't do any validation that you passed it a function or that the arity is a number. Both of these could cause errors or unexpected behavior. If we are planning to put this curry function into production it might be worth that additional sanity check.
Empty Arguments
If you need to support an implicit undefined
value, this version won't work for you.
const sum = (...args) => args.reduce((a,b) => a + b, 0);
const currySum = curry(sum, 4);
// Because we use ...args, we get an empty array.
// If we used the single arg, it would be undefined.
currySum()(1, 2)()(3, 4); // 10
You can pass an explicit undefined
as an argument, just not an implicit one. If you undo the work from the "Accept Multiple Arguments" section, your curry will accept implicit undefined, but of course loses the ability to pass multiple arguments at a time.
Too Many Arguments
Right now our curry function accepts more arguments than the arity.
const add = (...args) => args.reduce((a,b) => a + b, 0);
const addFour = curry(add, 4);
// What if I pass more than four?
addFour(1, 2, 3, 4, 5); // 15
Both of these considerations are consistent with the behavior of the Lodash curry function, so I'm not calling them errors, merely considerations. For use cases where an implicit undefined
or the exact number of arguments is required, it might be better to use a curry function which only accepts one argument at a time. If we switch back to using arg
instead of ...args
, both of these behaviors change.
Closing Remarks
I'm sure there are other issues and edge cases which aren't handled here, but I think our curry function works for most purposes. If something is missing, please comment and let me know!
It's been a long journey, but hopefully it was worth it to think through the design and decisions together. Thanks for reading!
Top comments (2)
This is an excellent article and is a great introduction.
When I did a similar article a couple of years ago I had the ability to pass a Missing Argument, meaning that you can create intermediates with any parameter missing - which is useful. Don't think I covered the arity stuff you did here.
Spice up your Javascript with some powerful curry! (Functional Programming and Currying)
Mike Talbot ⭐ ・ Aug 3 '21
Thanks! Combining partial application and currying together like you did is an interesting approach! I like the use of the Symbol for missing arguments. Older libraries like Lo-dash have you pass the library itself as the "missing" value, e.g.
_.partial(operation, _, 'two');
, which was useful before we had tools like Symbol.