JavaScript Currying: Functional Programming Technique
Master currying in JavaScript. Learn how to transform functions, create reusable utilities, and apply functional programming patterns.
JavaScript Currying: Functional Programming Technique
Currying is a functional programming technique that transforms a function with multiple arguments into a sequence of functions, each taking a single argument. It enables partial application, function composition, and creates more reusable and modular code.
What is Currying?
Currying transforms a function that takes multiple arguments into a series of functions that each take a single argument. Named after mathematician Haskell Curry, this technique is fundamental to functional programming.
// Regular function
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 6
// Curried version
function curriedAdd(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}
console.log(curriedAdd(1)(2)(3)); // 6
// Arrow function syntax
const curriedAddArrow = (a) => (b) => (c) => a + b + c;
console.log(curriedAddArrow(1)(2)(3)); // 6
// Partial application
const add1 = curriedAdd(1);
const add1And2 = add1(2);
console.log(add1And2(3)); // 6
How Currying Works
Manual Currying
// Two-argument function
function multiply(x, y) {
return x * y;
}
// Manually curried version
function curriedMultiply(x) {
return function (y) {
return x * y;
};
}
// Usage
const double = curriedMultiply(2);
console.log(double(5)); // 10
console.log(double(10)); // 20
const triple = curriedMultiply(3);
console.log(triple(5)); // 15
// Three-argument function
function greet(greeting, punctuation, name) {
return `${greeting}, ${name}${punctuation}`;
}
// Manually curried
function curriedGreet(greeting) {
return function (punctuation) {
return function (name) {
return `${greeting}, ${name}${punctuation}`;
};
};
}
// Create specialized functions
const sayHello = curriedGreet('Hello');
const sayHelloExcitedly = sayHello('!');
console.log(sayHelloExcitedly('Alice')); // Hello, Alice!
const sayGoodbye = curriedGreet('Goodbye');
const sayGoodbyeSadly = sayGoodbye('...');
console.log(sayGoodbyeSadly('Bob')); // Goodbye, Bob...
Automatic Currying
// Generic curry function
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function (...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
// Example usage
function sum(a, b, c, d) {
return a + b + c + d;
}
const curriedSum = curry(sum);
// All possible calling patterns
console.log(curriedSum(1, 2, 3, 4)); // 10
console.log(curriedSum(1)(2)(3)(4)); // 10
console.log(curriedSum(1, 2)(3, 4)); // 10
console.log(curriedSum(1)(2, 3)(4)); // 10
// Advanced curry with placeholder support
function curryWithPlaceholder(fn, placeholder = '_') {
return function curried(...args) {
const hasPlaceholder = args.includes(placeholder);
const validArgs = args.filter((arg) => arg !== placeholder);
if (!hasPlaceholder && args.length >= fn.length) {
return fn.apply(this, args);
}
return function (...nextArgs) {
const newArgs = args
.map((arg) =>
arg === placeholder && nextArgs.length > 0 ? nextArgs.shift() : arg
)
.concat(nextArgs);
return curried.apply(this, newArgs);
};
};
}
// Usage with placeholders
const _ = '_';
const curriedSubtract = curryWithPlaceholder((a, b, c) => a - b - c);
const subtractFrom10 = curriedSubtract(10, _, _);
console.log(subtractFrom10(3, 2)); // 10 - 3 - 2 = 5
const subtract5 = curriedSubtract(_, 5, _);
console.log(subtract5(10, 2)); // 10 - 5 - 2 = 3
Practical Applications
1. Configuration Functions
// Database query builder
const query = curry((table, conditions, fields) => {
const whereClause = Object.entries(conditions)
.map(([key, value]) => `${key} = '${value}'`)
.join(' AND ');
return `SELECT ${fields.join(', ')} FROM ${table} WHERE ${whereClause}`;
});
// Create specialized query functions
const queryUsers = query('users');
const queryActiveUsers = queryUsers({ active: true });
console.log(queryActiveUsers(['id', 'name', 'email']));
// SELECT id, name, email FROM users WHERE active = 'true'
// HTTP request builder
const request = curry((method, headers, url, data) => {
return fetch(url, {
method,
headers,
body: JSON.stringify(data),
});
});
// Create specialized request functions
const post = request('POST');
const postJSON = post({ 'Content-Type': 'application/json' });
const postToAPI = postJSON('https://api.example.com/data');
// Now just pass the data
// postToAPI({ name: 'John', age: 30 });
2. Data Transformation
// Array manipulation
const map = curry((fn, array) => array.map(fn));
const filter = curry((predicate, array) => array.filter(predicate));
const reduce = curry((reducer, initial, array) =>
array.reduce(reducer, initial)
);
// Create specialized transformations
const double = (x) => x * 2;
const isEven = (x) => x % 2 === 0;
const sum = (a, b) => a + b;
const doubleAll = map(double);
const filterEven = filter(isEven);
const sumAll = reduce(sum, 0);
// Usage
const numbers = [1, 2, 3, 4, 5];
console.log(doubleAll(numbers)); // [2, 4, 6, 8, 10]
console.log(filterEven(numbers)); // [2, 4]
console.log(sumAll(numbers)); // 15
// Compose transformations
const processNumbers = curry((transforms, data) =>
transforms.reduce((result, transform) => transform(result), data)
);
const pipeline = processNumbers([filterEven, doubleAll, sumAll]);
console.log(pipeline([1, 2, 3, 4, 5, 6])); // 24 (2*2 + 4*2 + 6*2)
3. Event Handling
// Event handler factory
const addEventListener = curry((eventType, handler, element) => {
element.addEventListener(eventType, handler);
return () => element.removeEventListener(eventType, handler);
});
// Create specialized event handlers
const onClick = addEventListener('click');
const onClickButton = onClick((event) => {
console.log('Button clicked:', event.target.textContent);
});
// Apply to multiple elements
const buttons = document.querySelectorAll('button');
const removeListeners = Array.from(buttons).map(onClickButton);
// Remove all listeners later
// removeListeners.forEach(remove => remove());
// Form validation
const validate = curry((rules, formData) => {
return Object.entries(rules).reduce((errors, [field, rule]) => {
const value = formData[field];
const error = rule(value);
if (error) errors[field] = error;
return errors;
}, {});
});
// Validation rules
const required = (value) => (!value ? 'This field is required' : null);
const minLength = curry((min, value) =>
value.length < min ? `Must be at least ${min} characters` : null
);
const email = (value) =>
!/^\S+@\S+\.\S+$/.test(value) ? 'Invalid email address' : null;
// Create form validator
const validateLogin = validate({
username: minLength(3),
password: minLength(8),
email: email,
});
console.log(
validateLogin({
username: 'ab',
password: 'pass',
email: 'invalid',
})
);
// { username: 'Must be at least 3 characters',
// password: 'Must be at least 8 characters',
// email: 'Invalid email address' }
4. Function Composition
// Compose function
const compose =
(...fns) =>
(x) =>
fns.reduceRight((result, fn) => fn(result), x);
// Pipe function (left to right)
const pipe =
(...fns) =>
(x) =>
fns.reduce((result, fn) => fn(result), x);
// Math operations
const add = curry((a, b) => a + b);
const multiply = curry((a, b) => a * b);
const subtract = curry((a, b) => a - b);
// Create complex operations
const add5 = add(5);
const multiplyBy3 = multiply(3);
const subtract2 = subtract(_, 2);
// Compose operations
const complexOperation = compose(subtract2, multiplyBy3, add5);
console.log(complexOperation(10)); // ((10 + 5) * 3) - 2 = 43
// String manipulation
const trim = (s) => s.trim();
const toLowerCase = (s) => s.toLowerCase();
const split = curry((delimiter, s) => s.split(delimiter));
const join = curry((delimiter, arr) => arr.join(delimiter));
const replace = curry((pattern, replacement, s) =>
s.replace(pattern, replacement)
);
// Create text processing pipeline
const processText = pipe(
trim,
toLowerCase,
replace(/[^\w\s]/g, ''),
split(' '),
filter((word) => word.length > 3),
join('-')
);
console.log(processText(' Hello, World! This IS a TEST... '));
// "hello-world-this-test"
Advanced Currying Patterns
1. Recursive Currying
// Variable argument currying
function curryN(n, fn) {
return function curried(...args) {
if (args.length >= n) {
return fn(...args);
}
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
// Auto-currying based on function length
function autoCurry(fn) {
const arity = fn.length;
return function curried(...args) {
if (args.length >= arity) {
return fn(...args);
}
return autoCurry((...nextArgs) => fn(...args, ...nextArgs));
};
}
// Infinite currying
function infiniteCurry(fn) {
const args = [];
return function curried(arg) {
if (arg === undefined) {
return fn(...args);
}
args.push(arg);
return curried;
};
}
// Usage
const sum = infiniteCurry((...args) => args.reduce((a, b) => a + b, 0));
console.log(sum(1)(2)(3)(4)(5)()); // 15 (call with no args to get result)
2. Context Preservation
// Curry with context binding
function curryWithContext(fn) {
return function curried(...args) {
const context = this;
if (args.length >= fn.length) {
return fn.apply(context, args);
}
return function (...nextArgs) {
return curried.apply(context, args.concat(nextArgs));
};
};
}
// Example with object methods
const calculator = {
base: 100,
add: curryWithContext(function (a, b) {
return this.base + a + b;
}),
multiply: curryWithContext(function (a, b) {
return this.base * a * b;
}),
};
const addToBase = calculator.add(10);
console.log(addToBase(5)); // 115 (100 + 10 + 5)
3. Type-Safe Currying
// Type checking curry
function typedCurry(fn, ...types) {
return function curried(...args) {
// Validate types
args.forEach((arg, i) => {
if (types[i] && typeof arg !== types[i]) {
throw new TypeError(
`Argument ${i} must be of type ${types[i]}, got ${typeof arg}`
);
}
});
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
// Usage
const safeDivide = typedCurry((a, b) => a / b, 'number', 'number');
try {
console.log(safeDivide(10)(2)); // 5
console.log(safeDivide('10')(2)); // TypeError
} catch (e) {
console.error(e.message);
}
4. Async Currying
// Curry for async functions
function curryAsync(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return async function (...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
};
}
// Example: API client
const apiRequest = curryAsync(async (method, endpoint, data) => {
const response = await fetch(`https://api.example.com${endpoint}`, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return response.json();
});
// Create specialized API functions
const post = apiRequest('POST');
const postUser = post('/users');
// Usage
// const newUser = await postUser({ name: 'John', email: 'john@example.com' });
// Sequential async operations
const processData = curryAsync(async (transform, validate, save, data) => {
const transformed = await transform(data);
const validation = await validate(transformed);
if (validation.isValid) {
return save(transformed);
}
throw new Error(validation.errors.join(', '));
});
Real-World Examples
1. React Component Enhancement
// HOC factory with currying
const withProps = curry((defaultProps, Component) => {
return function EnhancedComponent(props) {
return <Component {...defaultProps} {...props} />;
};
});
const withStyle = curry((styles, Component) => {
return function StyledComponent(props) {
return <Component {...props} style={{ ...styles, ...props.style }} />;
};
});
// Usage
const Button = ({ onClick, children, style }) => (
<button onClick={onClick} style={style}>
{children}
</button>
);
const PrimaryButton = pipe(
withStyle({ backgroundColor: 'blue', color: 'white' }),
withProps({ type: 'button' })
)(Button);
// Event handler factory
const createHandler = curry((action, dispatch, event) => {
event.preventDefault();
dispatch(action(event.target.value));
});
// In component
const handleChange = createHandler(updateValue, dispatch);
// <input onChange={handleChange} />
2. Data Processing Pipeline
// ETL pipeline with currying
const extract = curry((parser, source) => parser(source));
const transform = curry((rules, data) =>
rules.reduce((result, rule) => rule(result), data)
);
const load = curry((destination, data) => destination.save(data));
// CSV processing example
const parseCSV = (text) => text.split('\n').map((line) => line.split(','));
const transformRules = [
// Skip header
(data) => data.slice(1),
// Convert to objects
(rows) => rows.map(([name, age, city]) => ({ name, age: +age, city })),
// Filter adults
(users) => users.filter((user) => user.age >= 18),
// Sort by age
(users) => users.sort((a, b) => a.age - b.age),
];
// Create pipeline
const processCSV = pipe(
extract(parseCSV),
transform(transformRules),
load({ save: (data) => console.log('Saved:', data) })
);
// Usage
const csvData = `name,age,city
John,25,NYC
Jane,17,LA
Bob,30,Chicago`;
// processCSV(csvData);
3. Middleware Pattern
// Express-style middleware with currying
const middleware = curry((handler, req, res, next) => {
try {
const result = handler(req);
if (result instanceof Promise) {
result.then(next).catch(next);
} else {
next();
}
} catch (error) {
next(error);
}
});
// Authentication middleware
const authenticate = middleware((req) => {
const token = req.headers.authorization;
if (!token) throw new Error('No token provided');
const user = verifyToken(token);
req.user = user;
});
// Validation middleware
const validateBody = curry((schema, req) => {
const errors = schema.validate(req.body);
if (errors.length) throw new ValidationError(errors);
});
const validateUser = middleware(
validateBody({
validate: (body) => {
const errors = [];
if (!body.email) errors.push('Email required');
if (!body.password) errors.push('Password required');
return errors;
},
})
);
// Logging middleware
const logger = middleware((req) => {
console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
});
4. Configuration Builder
// Flexible configuration with currying
const createConfig = curry((defaults, overrides, environment) => {
const envConfig =
{
development: { debug: true, logLevel: 'verbose' },
production: { debug: false, logLevel: 'error' },
}[environment] || {};
return { ...defaults, ...envConfig, ...overrides };
});
// Database config builder
const dbConfig = createConfig({
host: 'localhost',
port: 5432,
ssl: false,
});
const devDB = dbConfig({}, 'development');
const prodDB = dbConfig({ ssl: true }, 'production');
// API client builder
const createAPIClient = curry((baseURL, defaultHeaders, endpoints) => {
const client = {};
Object.entries(endpoints).forEach(([name, config]) => {
client[name] = curry(async (params, data) => {
const url =
baseURL + config.path.replace(/:(\w+)/g, (_, key) => params[key]);
const response = await fetch(url, {
method: config.method,
headers: { ...defaultHeaders, ...config.headers },
body: data ? JSON.stringify(data) : undefined,
});
return response.json();
});
});
return client;
});
// Usage
const apiClient = createAPIClient('https://api.example.com', {
'Content-Type': 'application/json',
})({
getUser: { method: 'GET', path: '/users/:id' },
updateUser: { method: 'PUT', path: '/users/:id' },
createUser: { method: 'POST', path: '/users' },
});
// const user = await apiClient.getUser({ id: 123 });
// const updated = await apiClient.updateUser({ id: 123 }, { name: 'New Name' });
Performance Considerations
Memory Usage
// Be careful with closure memory retention
function memoryEfficientCurry(fn) {
return function curried(...args) {
// Don't store args in outer scope
if (args.length >= fn.length) {
return fn(...args);
}
return function (...nextArgs) {
// Create new array instead of closure over args
return curried(...[...args, ...nextArgs]);
};
};
}
// Benchmarking curried vs regular functions
function benchmark(name, fn, iterations = 1000000) {
console.time(name);
for (let i = 0; i < iterations; i++) {
fn();
}
console.timeEnd(name);
}
const regularAdd = (a, b, c) => a + b + c;
const curriedAdd = curry((a, b, c) => a + b + c);
const partialAdd = curriedAdd(1)(2);
benchmark('Regular', () => regularAdd(1, 2, 3));
benchmark('Curried', () => curriedAdd(1)(2)(3));
benchmark('Partial', () => partialAdd(3));
Best Practices
1. When to Use Currying
// Good use cases:
// 1. Configuration and specialization
const configure = curry((options, instance) => ({ ...instance, ...options }));
const withAuth = configure({ authenticated: true });
// 2. Event handlers with data
const handleClick = curry((data, event) => {
console.log('Clicked with data:', data);
});
// 3. Array method chains
const processArray = pipe(
map((x) => x * 2),
filter((x) => x > 10),
reduce((a, b) => a + b, 0)
);
// Avoid currying when:
// 1. Function has variable arguments
// 2. Performance is critical
// 3. Code readability suffers
2. Naming Conventions
// Clear naming for curried functions
const createValidator = curry((rules, value) => {
/* ... */
});
const validateEmail = createValidator(emailRules);
// Indicate partial application
const addPrefix = curry((prefix, str) => prefix + str);
const addDr = addPrefix('Dr. '); // Clear what's partially applied
// Use descriptive names for each level
const makeHttpRequest = (method) => (url) => (data) => {
/* ... */
};
const get = makeHttpRequest('GET');
const getFromAPI = get('/api/users');
3. Documentation
/**
* Creates a formatter function for numbers
* @param {number} decimals - Number of decimal places
* @param {string} prefix - String to prepend
* @param {string} suffix - String to append
* @returns {Function} Formatter function that takes a number
*
* @example
* const formatCurrency = createFormatter(2, '$', '');
* formatCurrency(1234.5); // "$1234.50"
*/
const createFormatter = curry((decimals, prefix, suffix, value) => {
return `${prefix}${value.toFixed(decimals)}${suffix}`;
});
Conclusion
Currying is a powerful functional programming technique that enables:
- Function specialization through partial application
- Code reusability by creating variations of functions
- Function composition for building complex operations
- Cleaner APIs with configuration patterns
- Better separation of concerns in function design
Key takeaways:
- Currying transforms multi-argument functions into a series of single-argument functions
- It enables partial application and function composition
- Use automatic currying functions for flexibility
- Consider performance implications for hot code paths
- Apply currying where it improves code clarity and reusability
Best practices:
- Use currying for configuration and specialization
- Keep curried functions simple and focused
- Document the expected usage patterns
- Consider readability for your team
- Don't over-curry simple functions
Master currying to write more functional, composable, and maintainable JavaScript code!