Function Currying: The Art of Parameter Step-by-Step Processing
Imagine you're ordering coffee at a custom coffee shop. The regular way to order is to state all your requirements at once: "I want a large latte with double shot, oat milk, no sugar." But some baristas confirm step by step: "First, what type of drink?" "Latte." "Size?" "Large." "Shot count?" "Double." This step-by-step processing approach is similar to function currying—instead of receiving all parameters at once, it receives a portion of parameters each time, then returns a new function to handle the remaining parameters.
What Is Function Currying
Function Currying is a function transformation technique that converts a function that accepts multiple parameters into a series of functions that accept single parameters. The name "currying" comes from mathematician Haskell Curry, though the concept was actually first proposed by Moses Schönfinkel.
Let's understand currying through a simple example:
// Regular function: receives all parameters at once
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 6
// Curried function: receives one parameter at a time
function addCurried(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}
console.log(addCurried(1)(2)(3)); // 6
// Concise version using arrow functions
const addCurriedArrow = (a) => (b) => (c) => a + b + c;
console.log(addCurriedArrow(1)(2)(3)); // 6The regular add function receives three parameters a, b, and c at once, then returns their sum. The curried version addCurried receives the first parameter a, returns a new function; this new function receives the second parameter b, returns another new function; finally this function receives the third parameter c and calculates the final result.
The calling method also changes from add(1, 2, 3) to addCurried(1)(2)(3). Each call returns a new function until all parameters are received.
Core Principles of Currying
The core of currying lies in closures. Each returned function remembers the previously passed parameters, which are stored in closures until all parameters are collected for final computation.
Let's look at an example in detail:
function multiply(a) {
console.log(`Received first argument: ${a}`);
return function (b) {
console.log(`Received second argument: ${b}`);
return function (c) {
console.log(`Received third argument: ${c}`);
console.log(`Computing: ${a} * ${b} * ${c}`);
return a * b * c;
};
};
}
// Step-by-step calling
const step1 = multiply(2);
console.log("After first call, returned a function");
const step2 = step1(3);
console.log("After second call, returned another function");
const result = step2(4);
console.log(`Final result: ${result}`);
// Output:
// Received first argument: 2
// After first call, returned a function
// Received second argument: 3
// After second call, returned another function
// Received third argument: 4
// Computing: 2 * 3 * 4
// Final result: 24Each function maintains references to previous parameters. When we call multiply(2), parameter a = 2 is stored in the closure. The returned function can access this a. When we call step1(3), parameter b = 3 is stored while still being able to access a. Finally, when we call step2(4), all three parameters are available to execute the final computation.
Implementing a Generic Currying Function
Manually currying every function is tedious. We can create a generic currying function that automatically converts any function to a curried version.
Basic Currying Implementation
function curry(fn) {
return function curried(...args) {
// If enough parameters are passed, execute the original function directly
if (args.length >= fn.length) {
return fn.apply(this, args);
}
// Otherwise return a new function to wait for more parameters
return function (...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
};
}
// Example: regular function
function sum(a, b, c) {
return a + b + c;
}
// Convert to curried function
const curriedSum = curry(sum);
// Multiple calling methods all work
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
console.log(curriedSum(1, 2, 3)); // 6This curry function checks the number of parameters currently received. If the parameter count has reached what the original function needs (obtained through fn.length), it executes the original function. Otherwise, it returns a new function that merges new parameters with previous ones and recursively calls curried.
Advanced Currying with Placeholder Support
Sometimes we want to skip certain parameters and fill them in later. We can use placeholders to implement this:
function advancedCurry(fn, placeholder = Symbol("placeholder")) {
return function curried(...args) {
// Check if there are enough actual parameters (excluding placeholders)
const hasPlaceholder = args.some((arg) => arg === placeholder);
const validArgs = args.filter((arg) => arg !== placeholder);
if (!hasPlaceholder && args.length >= fn.length) {
return fn.apply(this, args);
}
return function (...nextArgs) {
// Merge parameters, using new parameters to replace placeholders
let mergedArgs = args.map((arg) =>
arg === placeholder && nextArgs.length > 0 ? nextArgs.shift() : arg
);
// Add remaining new parameters
mergedArgs = mergedArgs.concat(nextArgs);
return curried.apply(this, mergedArgs);
};
};
}
// Use placeholder
const _ = Symbol("placeholder");
const curriedSubtract = advancedCurry((a, b, c) => a - b - c, _);
// Can skip middle parameters
const subtractFrom10 = curriedSubtract(10);
const subtract5From10 = subtractFrom10(5);
console.log(subtract5From10(2)); // 3 - (10 - 5 - 2)
// Use placeholder to skip first parameter
const subtractFromPlaceholder = curriedSubtract(_, 5, 2);
console.log(subtractFromPlaceholder(10)); // 3 - (10 - 5 - 2)Practical Applications of Currying
1. Parameter Reuse
A primary use of currying is to create specific configured variants of functions, implementing parameter reuse.
// Function for formatting logs
function log(level, timestamp, message) {
console.log(`[${level}] ${timestamp}: ${message}`);
}
// Curry
const curriedLog = curry(log);
// Create log functions for specific levels
const logError = curriedLog("ERROR");
const logWarning = curriedLog("WARNING");
const logInfo = curriedLog("INFO");
// Create log functions with timestamps
const now = new Date().toISOString();
const logErrorNow = logError(now);
const logWarningNow = logWarning(now);
// Usage
logErrorNow("Database connection failed");
// [ERROR] 2025-12-04T14:51:20.000Z: Database connection failed
logWarningNow("API response time exceeds threshold");
// [WARNING] 2025-12-04T14:51:20.000Z: API response time exceeds threshold
logInfo(now)("Application started successfully");
// [INFO] 2025-12-04T14:51:20.000Z: Application started successfullyThrough currying, we created pre-configured log functions. logError fixed the log level as "ERROR", logErrorNow further fixed the timestamp. This avoids repeatedly passing the same parameters.
2. Delayed Execution
Currying allows you to gradually collect parameters and execute the function at the appropriate time.
// Calculate discounted price
function calculatePrice(basePrice, taxRate, discount) {
const priceWithTax = basePrice * (1 + taxRate);
const finalPrice = priceWithTax * (1 - discount);
return finalPrice.toFixed(2);
}
const curriedPrice = curry(calculatePrice);
// Set basic configuration (tax rates might differ in different regions)
const priceInNY = curriedPrice(_, 0.08); // New York tax rate
const priceInCA = curriedPrice(_, 0.0725); // California tax rate
// Set discounts (different membership levels)
const premiumUserInNY = priceInNY(_, 0.2); // Premium member 20% off
const regularUserInNY = priceInNY(_, 0.1); // Regular member 10% off
// Calculate specific product prices
console.log(premiumUserInNY(100)); // $86.40
console.log(regularUserInNY(100)); // $97.20
console.log(priceInCA(100, 0.15)); // $91.163. Event Handling
In event handling, currying can elegantly pass additional parameters.
// Generic event handler
function handleEvent(eventType, handler, element) {
element.addEventListener(eventType, handler);
return () => element.removeEventListener(eventType, handler);
}
const curriedHandle = curry(handleEvent);
// Create handlers for specific event types
const handleClick = curriedHandle("click");
const handleHover = curriedHandle("mouseover");
// Create handlers with specific handling logic
const handleClickWithLog = handleClick((e) => {
console.log("Element clicked:", e.target);
});
// Apply to specific elements (assuming browser environment)
/*
const button1 = document.getElementById("submit-btn");
const button2 = document.getElementById("cancel-btn");
const removeListener1 = handleClickWithLog(button1);
const removeListener2 = handleClickWithLog(button2);
// Later can remove listeners
removeListener1();
removeListener2();
*/4. Preparation for Function Composition
Curried functions are easier to compose because each function returns a single-parameter function.
// Data processing functions
const multiply = curry((factor, value) => value * factor);
const add = curry((addition, value) => value + addition);
const divide = curry((divisor, value) => value / divisor);
// Create specific transformation functions
const double = multiply(2);
const addTen = add(10);
const halve = divide(2);
// Compose functions
function compose(...fns) {
return function (value) {
return fns.reduceRight((acc, fn) => fn(acc), value);
};
}
const transform = compose(addTen, double, halve);
console.log(transform(20)); // 30
// Process:
// halve(20) = 10
// double(10) = 20
// addTen(20) = 305. Form Validation
Currying is very useful when building configurable validation functions.
// Generic validation function
const validate = curry((validator, errorMessage, value) => {
return validator(value)
? { valid: true }
: { valid: false, error: errorMessage };
});
// Specific validators
const minLength = (min) => (str) => str.length >= min;
const maxLength = (max) => (str) => str.length <= max;
const hasPattern = (pattern) => (str) => pattern.test(str);
// Create specific validation functions
const validateMinLength = validate(
minLength(8),
"Must be at least 8 characters"
);
const validateMaxLength = validate(
maxLength(20),
"Must be at most 20 characters"
);
const validateHasUpperCase = validate(
hasPattern(/[A-Z]/),
"Must contain at least one uppercase letter"
);
const validateHasNumber = validate(
hasPattern(/[0-9]/),
"Must contain at least one number"
);
// Combine validations
function validatePassword(password) {
const validators = [
validateMinLength,
validateMaxLength,
validateHasUpperCase,
validateHasNumber,
];
for (let validator of validators) {
const result = validator(password);
if (!result.valid) {
return result;
}
}
return { valid: true };
}
console.log(validatePassword("weak"));
// { valid: false, error: "Must be at least 8 characters" }
console.log(validatePassword("WeakPassword"));
// { valid: false, error: "Must contain at least one number" }
console.log(validatePassword("StrongPass123"));
// { valid: true }6. API Request Building
Currying can help build flexible API request functions.
// Generic API request function
const apiRequest = curry((baseURL, method, endpoint, data) => {
const url = `${baseURL}${endpoint}`;
const options = {
method: method,
headers: {
"Content-Type": "application/json",
},
};
if (data) {
options.body = JSON.stringify(data);
}
return fetch(url, options).then((res) => res.json());
});
// Configure base URL
const api = apiRequest("https://api.example.com");
// Configure different request methods
const get = api("GET");
const post = api("POST");
const put = api("PUT");
const del = api("DELETE");
// Create request functions for specific resources
const getUsers = get("/users");
const createUser = post("/users");
const updateUser = put("/users");
// Usage
// getUsers(); // GET https://api.example.com/users
// createUser({ name: "John", email: "[email protected]" });
// updateUser({ id: 1, name: "John Updated" });
// More specific functions
const getUserById = (id) => get(`/users/${id}`)();
// getUserById(123); // GET https://api.example.com/users/123Currying vs Partial Application
Currying is often confused with Partial Application. They are similar but different:
Currying: Converts an n-ary function into a nested sequence of n unary functions. Each time only one parameter is received.
Partial Application: Fixes part of a function's parameters and returns a new function that receives the remaining parameters. Multiple parameters can be fixed at once.
// Original function
function greet(greeting, title, firstName, lastName) {
return `${greeting}, ${title} ${firstName} ${lastName}!`;
}
// Currying: one parameter at a time
const curriedGreet = curry(greet);
const hello = curriedGreet("Hello");
const helloMr = hello("Mr.");
const helloMrJohn = helloMr("John");
console.log(helloMrJohn("Doe")); // Hello, Mr. John Doe!
// Partial application: can fix multiple parameters at once
function partial(fn, ...fixedArgs) {
return function (...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}
const greetMr = partial(greet, "Hello", "Mr.");
console.log(greetMr("John", "Doe")); // Hello, Mr. John Doe!
console.log(greetMr("Jane", "Smith")); // Hello, Mr. Jane Smith!
// Partial application can fix middle parameters (using placeholders)
const greetDoe = partial(greet, "Hi", _, _, "Doe");
// But this requires more complex implementationCurrying always returns nested unary functions with calling syntax f(a)(b)(c). Partial application can flexibly fix any number of parameters with calling syntax f(a, b, c) or f(a)(b, c), etc.
In practice, many libraries (like Lodash's _.curry) actually implement a hybrid version that supports both currying's single-parameter calls and multi-parameter partial application.
Performance Considerations
Although currying is powerful, it also has performance costs. Every curried call creates new closures and functions, which consumes memory and execution time.
// Performance test example
function normalAdd(a, b, c) {
return a + b + c;
}
const curriedAdd = (a) => (b) => (c) => a + b + c;
// Test regular function
console.time("Normal");
for (let i = 0; i < 1000000; i++) {
normalAdd(1, 2, 3);
}
console.timeEnd("Normal"); // Normal: ~5ms
// Test curried function
console.time("Curried");
for (let i = 0; i < 1000000; i++) {
curriedAdd(1)(2)(3);
}
console.timeEnd("Curried"); // Curried: ~15msCurried functions are typically 2-3 times slower than regular functions. In performance-critical code paths (like animation loops that execute every frame), you should avoid overusing currying.
Optimization suggestions:
- Use only when needed: Don't curry for the sake of currying; only use it in scenarios that truly benefit
- Cache curried results: If a partially applied function will be reused repeatedly, cache it
- Use in development, optimize in production: Currying improves development experience, but consider de-currying in performance bottlenecks
// Cache curried results to improve performance
const logger = curry(log);
const logError = logger("ERROR"); // Cache this partially applied function
// Reuse in loops
for (let i = 0; i < 1000; i++) {
logError(new Date().toISOString())(`Processing item ${i}`);
}Common Pitfalls
1. Parameter Order is Important
The parameter order of curried functions should be arranged from "least likely to change" to "most likely to change".
// ❌ Bad parameter order
const badFormat = curry((value, format) => {
return format.replace("{value}", value);
});
// Have to pass value first, not flexible
const formatNumber = badFormat(123);
console.log(formatNumber("Value: {value}")); // Value: 123
console.log(formatNumber("Number: {value}")); // Number: 123
// ✅ Good parameter order
const goodFormat = curry((format, value) => {
return format.replace("{value}", value);
});
// Can reuse format
const priceFormat = goodFormat("Price: ${value}");
console.log(priceFormat(99.99)); // Price: $99.99
console.log(priceFormat(149.99)); // Price: $149.99General rule: put configuration parameters first, data parameters second.
2. Over-currying
Not all functions need to be curried. Overuse can make code hard to understand.
// ❌ Over-currying
const calculate = (a) => (b) => (c) => (d) => (e) => (f) => {
return ((a + b) * c - d) / e + f;
};
console.log(calculate(1)(2)(3)(4)(5)(6)); // Hard to read
// ✅ Reasonable grouping
const calculate2 = curry((a, b, c, d, e, f) => {
return ((a + b) * c - d) / e + f;
});
console.log(calculate2(1, 2)(3)(4, 5, 6)); // Clearer3. Debugging Difficulties
The nested structure of currying can make debugging difficult.
// Hard to debug
const complexOperation = curry((a, b, c, d) => {
// If error occurs here, stack trace will be deep
return a + b + c + d;
});
// Adding intermediate logs helps debugging
const debugCurry = curry((a, b, c, d) => {
console.log("Arguments:", { a, b, c, d });
const result = a + b + c + d;
console.log("Result:", result);
return result;
});Summary
Function currying is a powerful tool in functional programming that provides greater flexibility by converting multi-parameter functions into nested single-parameter functions.
Key takeaways:
- Currying converts
f(a, b, c)tof(a)(b)(c) - Core mechanism is closures; each function remembers previous parameters
- Main advantages: parameter reuse, delayed execution, function composition
- Different from partial application: currying is always single-parameter, partial application can be multi-parameter
- Parameter order is important: from unchanging to changing
- Has performance costs; use cautiously in performance-critical code
- Don't overuse; only in scenarios that truly benefit
Currying makes code more modular and reusable. When used with pure functions, it can build clear, testable functional code. In the next chapter, we'll learn about function composition, which combined with currying can create powerful data processing pipelines.