dryrun

Currying in JavaScript

javascript

What is Currying?

Imagine you have a function that needs three numbers to work: add(a, b, c). Normally, you’d call it like add(2, 3, 5) and get the answer right away.

Regular Function— all args at once
add(2,3,5)
10

Currying lets you break this into smaller steps:

1/4Curried Function: Pass first Argument
add(2)
Returns:
fn(b) => /* waits for b, c */
a = 2

OK, but why?

Currying is particularly useful when you want to create specialized functions from more general ones. Consider an API client where you need to make requests to different endpoints with consistent configuration. Before, without currying:

const request = async (baseUrl, method, endpoint, body) => {
  const response = await fetch(`${baseUrl}${endpoint}`, {
    method,
    body: body ? JSON.stringify(body) : undefined,
  });
  return response.json();
};

// Every call requires repeating params
await request("https://api.example.com", "GET", "/users", null);
await request("https://api.example.com", "POST", "/users", { name: "Alice" });
await request("https://api.example.com", "GET", "/posts", null);

After, with currying:

const _request = async (baseUrl, method, endpoint, body) => {
  const response = await fetch(`${baseUrl}${endpoint}`, {
    method,
    body: body ? JSON.stringify(body) : undefined,
  });
  return response.json();
};
const request = curry(_request);

// Configure once, use everywhere
const api = request("https://api.example.com");
const get = api("GET");
const post = api("POST");

await get("/users");
await post("/users", { name: "Alice" });
await get("/posts");

Implementation

So, how do we implement the curry function from the previous example?

The key idea is to check if we have received enough arguments yet. Every function in JavaScript has a .length property that tells us how many parameters it expects. When someone calls our curried function, we compare the number of arguments they passed against this expected count.

If we have all the arguments we need, we simply call the original function with them. But if we’re still missing some, we return a new function that captures the arguments we’ve received so far in a closure. When this new function is called with more arguments, it combines them with the ones it captured in the closure and checks again. This process repeats until we’ve accumulated enough arguments to finally execute the original function.

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function (...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

Dry Run: Tracing add(1)(2)(3)

Let’s trace through exactly what happens when we call our curried add function:

1/7Setup
First, we define our function and curry it:
const _add = (a, b, c) => a + b + c← 3 params
const add = curry(_add)← wrap it
function curry(func) {← called with _add
return function curried(...args) {← returns this
if (args.length >= func.length)
return func.apply(this, args)
else
return function (...args2) {
return curried(...args.concat(args2))
}
}
}
_add.length:3add:curried fn