JavaScript Series #95: Currying and Partial Application
In the realm of functional programming within JavaScript, two powerful concepts often emerge: Currying and Partial Application. While often confused due to their similar outcomes – creating specialized functions from more general ones – they represent distinct techniques for manipulating how functions receive and process arguments. Mastering these patterns can lead to more declarative, reusable, and maintainable codebases.
Understanding Currying in JavaScript
Currying is a transformation of functions that takes multiple arguments into a sequence of functions, each taking a single argument. This transformation allows you to invoke a function step-by-step, receiving a new function after each argument is provided, until all arguments have been received and the final result is returned.
Imagine a function that takes three arguments. A curried version of this function would first take one argument and return a new function. This new function would then take the second argument and return yet another function. Finally, the third function would take the last argument and execute the original logic.
Why Use Currying?
- Readability and Reusability: It promotes creating specialized versions of a general function, making your code more modular and easier to understand.
- Function Composition: Curried functions are ideal for function composition, where the output of one function becomes the input of another.
- Delaying Execution: You can delay the execution of a function until all necessary arguments are available.
Currying in Action: Manual Implementation
Let's start with a simple function that adds three numbers.
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // Output: 6
Now, let's manually curry this function:
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
// Invoking the curried function
console.log(curriedAdd(1)(2)(3)); // Output: 6
// Creating specialized functions
const addOne = curriedAdd(1);
const addOneAndTwo = addOne(2);
console.log(addOneAndTwo(3)); // Output: 6
const addFive = curriedAdd(5);
console.log(addFive(10)(20)); // Output: 35
Building a Generic Curry Function
Manually currying every function can be tedious. A generic curry helper function can automate this process. There are many ways to implement a curry helper, often involving recursion or managing argument lists. Here's a common approach:
function curry(func) {
return function curried(...args) {
// If enough arguments are provided, execute the original function
if (args.length >= func.length) {
return func(...args);
} else {
// Otherwise, return a new function that expects the remaining arguments
return function(...nextArgs) {
return curried(...args, ...nextArgs);
};
}
};
}
// Using our generic curry function
function multiply(a, b, c) {
return a * b * c;
}
const curriedMultiply = curry(multiply);
console.log(curriedMultiply(2)(3)(4)); // Output: 24
console.log(curriedMultiply(2, 3)(4)); // Output: 24
console.log(curriedMultiply(2, 3, 4)); // Output: 24
const multiplyByTwo = curriedMultiply(2);
const multiplyByTwoAndThree = multiplyByTwo(3);
console.log(multiplyByTwoAndThree(5)); // Output: 30
Demystifying Partial Application
Partial Application (or partial function application) is the process of creating a new function by pre-filling some of the arguments of an existing function. The newly created function takes the remaining arguments to complete the computation.
Unlike currying, which transforms a function to take *one* argument at a time, partial application allows you to fix *any number* of initial arguments. The resulting function will then expect the rest.
Partial Application with .bind()
JavaScript's built-in Function.prototype.bind() method is a perfect tool for partial application. While primarily used for setting the this context, any arguments passed after the this context will be pre-filled.
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
// Partially apply 'Hi' as the first argument
const sayHi = greet.bind(null, 'Hi');
console.log(sayHi('Alice')); // Output: Hi, Alice!
console.log(sayHi('Bob')); // Output: Hi, Bob!
// Partially apply 'Hello' and 'World'
const helloWorld = greet.bind(null, 'Hello', 'World');
console.log(helloWorld()); // Output: Hello, World! (no further arguments needed)
function calculate(operation, a, b) {
switch (operation) {
case 'add': return a + b;
case 'subtract': return a - b;
default: return 'Invalid operation';
}
}
const addNumbers = calculate.bind(null, 'add');
console.log(addNumbers(5, 3)); // Output: 8
console.log(addNumbers(10, 20)); // Output: 30
const subtractNumbers = calculate.bind(null, 'subtract');
console.log(subtractNumbers(10, 5)); // Output: 5
Custom Partial Application Helper
You can also create a custom helper function for partial application, especially if you want more control or want to avoid .bind()'s this context manipulation.
function partial(func, ...fixedArgs) {
return function(...remainingArgs) {
return func(...fixedArgs, ...remainingArgs);
};
}
function power(base, exponent) {
return Math.pow(base, exponent);
}
// Create a function to square numbers (base fixed at 2)
const square = partial(power, 2); // Incorrect application, power expects (base, exponent)
// This would fix base=2, so it's 2^exponent
console.log(square(3)); // Output: 8 (2^3)
console.log(square(4)); // Output: 16 (2^4)
// Let's correct for squaring (exponent fixed at 2)
function powerCorrected(exponent, base) { // Reorder arguments for partial application
return Math.pow(base, exponent);
}
const squareNumbers = partial(powerCorrected, 2); // fix exponent to 2
console.log(squareNumbers(3)); // Output: 9 (3^2)
console.log(squareNumbers(4)); // Output: 16 (4^2)
// Example for cubing numbers
const cubeNumbers = partial(powerCorrected, 3); // fix exponent to 3
console.log(cubeNumbers(2)); // Output: 8 (2^3)
Currying vs. Partial Application: The Key Differences
While both techniques are used to create specialized functions by pre-filling arguments, their fundamental approaches differ:
-
Currying:
- Transforms a function of N arguments into N functions, each taking a single argument.
- Always returns a new function until all arguments are satisfied, one by one.
- Enforces a strict arity (number of arguments) and argument order.
- Aims at full transformation, making functions more composable in a pipeline.
-
Partial Application:
- Creates a new function by fixing a subset of the original function's arguments.
- The new function can still take multiple remaining arguments, not necessarily one by one.
- Does not necessarily change the arity of the arguments it receives in subsequent calls, just reduces the total number of arguments it expects.
- More flexible in which arguments are fixed and how many.
Think of it this way: currying is like an assembly line where each station adds one component. Partial application is like pre-assembling a few components at once before sending it to the rest of the line.
Practical Use Cases and Benefits
Creating Specialized Functions
Both techniques excel at transforming generic utilities into highly specialized, domain-specific functions.
// Generic logger
function logger(level, tag, message) {
console.log(`[${level}][${tag}] ${message}`);
}
// Using partial application to create specialized loggers
const warnUser = partial(logger, 'WARN', 'USER_ACTIVITY');
const errorSystem = partial(logger, 'ERROR', 'SYSTEM_FAILURE');
warnUser('Invalid input detected.');
errorSystem('Database connection lost.');
// Using currying for more granular control (if `logger` were curried)
// function curriedLogger(level) {
// return function(tag) {
// return function(message) {
// console.log(`[${level}][${tag}] ${message}`);
// };
// };
// }
// const logWarn = curriedLogger('WARN');
// const logError = curriedLogger('ERROR');
// logWarn('USER_ACTIVITY')('Invalid input detected.');
Event Handlers
Partial application is very useful for dynamically creating event handlers that need access to specific data.
// Assume a function to handle clicks on items
function handleItemClick(itemId, event) {
console.log(`Item ${itemId} was clicked! Event:`, event.type);
}
// In a real DOM scenario, you might do:
// document.getElementById('item-1').addEventListener('click', handleItemClick.bind(null, 'item-1-data'));
// document.getElementById('item-2').addEventListener('click', handleItemClick.bind(null, 'item-2-data'));
// Example usage without actual DOM:
const clickHandlerItem1 = handleItemClick.bind(null, 'item-1-data');
const clickHandlerItem2 = handleItemClick.bind(null, 'item-2-data');
clickHandlerItem1({ type: 'click', target: 'div#item-1' });
clickHandlerItem2({ type: 'click', target: 'div#item-2' });
Functional Composition
Currying makes functions incredibly amenable to composition, especially when combined with utility functions like pipe or compose from libraries like Lodash/Ramda, or custom implementations.
const curriedAdd = curry((a, b) => a + b);
const curriedMultiply = curry((a, b) => a * b);
const add5 = curriedAdd(5); // function(b) { return 5 + b; }
const multiplyBy2 = curriedMultiply(2); // function(b) { return 2 * b; }
// A simple compose function (reads right-to-left)
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
// Add 5, then multiply by 2
const add5ThenMultiplyBy2 = compose(multiplyBy2, add5);
console.log(add5ThenMultiplyBy2(10)); // Output: 30 ((10 + 5) * 2)
// Multiply by 2, then add 5
const multiplyBy2ThenAdd5 = compose(add5, multiplyBy2);
console.log(multiplyBy2ThenAdd5(10)); // Output: 25 ((10 * 2) + 5)
Conclusion
Currying and partial application are invaluable techniques in a JavaScript developer's toolkit, especially when adopting a more functional programming paradigm. They empower you to write more flexible, declarative, and reusable code by abstracting argument handling. While currying meticulously breaks down a function into a series of single-argument calls, partial application offers the flexibility to pre-fill any number of arguments, creating specialized versions of existing functions. Understanding when and how to apply each technique will undoubtedly lead to cleaner, more expressive, and robust JavaScript applications. Experiment with them in your projects to see the benefits firsthand!