JavaScript Closures Tutorial - Section 2: Practical Patterns
Currying is the process of transforming a function that takes multiple arguments into a sequence of functions that each take a single argument. It's named after mathematician Haskell Curry.
// 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
// Or with arrow functions
const curriedAdd2 = a => b => c => a + b + c;
console.log(curriedAdd2(1)(2)(3)); // 6
Currying allows you to create specialized functions by pre-filling some arguments:
const curriedMultiply = a => b => a * b;
// Create specialized functions
const double = curriedMultiply(2);
const triple = curriedMultiply(3);
const quadruple = curriedMultiply(4);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20
const log = level => prefix => message => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}] [${prefix}] ${message}`);
};
// Create specialized loggers
const infoLog = log('INFO');
const errorLog = log('ERROR');
const apiInfo = infoLog('API');
const apiError = errorLog('API');
const dbInfo = infoLog('DB');
const dbError = errorLog('DB');
// Use them
apiInfo('Request received'); // [timestamp] [INFO] [API] Request received
apiError('Connection failed'); // [timestamp] [ERROR] [API] Connection failed
dbInfo('Query executed'); // [timestamp] [INFO] [DB] Query executed
dbError('Transaction rolled back'); // [timestamp] [ERROR] [DB] Transaction rolled back
Partial application is similar to currying but more flexible - you can fix any number of arguments:
function partial(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}
// Original function
function greet(greeting, name, punctuation) {
return `${greeting}, ${name}${punctuation}`;
}
// Create partially applied functions
const sayHello = partial(greet, 'Hello');
const sayHelloToAlice = partial(greet, 'Hello', 'Alice');
console.log(sayHello('Bob', '!')); // "Hello, Bob!"
console.log(sayHelloToAlice('!')); // "Hello, Alice!"
console.log(sayHelloToAlice('?')); // "Hello, Alice?"
const makeRequest = method => url => headers => body => {
return fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
...headers
},
body: body ? JSON.stringify(body) : undefined
}).then(res => res.json());
};
// Create specialized request functions
const get = makeRequest('GET');
const post = makeRequest('POST');
const put = makeRequest('PUT');
const del = makeRequest('DELETE');
// Create API endpoint functions
const apiGet = get('https://api.example.com');
const apiPost = post('https://api.example.com');
// Create authenticated versions
const authHeaders = { 'Authorization': 'Bearer token123' };
const authGet = apiGet(authHeaders);
const authPost = apiPost(authHeaders);
// Use them
authGet(null); // GET request with auth
authPost({ name: 'Alice', email: 'alice@example.com' }); // POST with auth and body
Create a utility that automatically curries any function:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
}
};
}
// Use it with any function
function sum(a, b, c, d) {
return a + b + c + d;
}
const curriedSum = curry(sum);
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
const validate = rule => value => {
return rule(value);
};
// Define validation rules
const isRequired = value => value !== null && value !== undefined && value !== '';
const minLength = min => value => value.length >= min;
const maxLength = max => value => value.length <= max;
const isEmail = value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
const isNumeric = value => /^\d+$/.test(value);
// Create specific validators
const validateRequired = validate(isRequired);
const validateMinLength5 = validate(minLength(5));
const validateMaxLength20 = validate(maxLength(20));
const validateEmail = validate(isEmail);
// Compose validators
function composeValidators(...validators) {
return value => {
for (const validator of validators) {
if (!validator(value)) {
return false;
}
}
return true;
};
}
const validateUsername = composeValidators(
validateRequired,
validateMinLength5,
validateMaxLength20
);
const validateEmailField = composeValidators(
validateRequired,
validateEmail
);
console.log(validateUsername('alice')); // false (too short)
console.log(validateUsername('alice123')); // true
console.log(validateEmailField('alice')); // false (not email)
console.log(validateEmailField('alice@example.com')); // true
const createConfig = baseUrl => timeout => retries => headers => {
return {
baseUrl,
timeout,
retries,
headers,
request: async function(endpoint, options = {}) {
let attempt = 0;
while (attempt < this.retries) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: { ...this.headers, ...options.headers },
signal: controller.signal
});
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
attempt++;
if (attempt >= this.retries) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
};
};
// Build configurations step by step
const withBaseUrl = createConfig('https://api.example.com');
const withTimeout = withBaseUrl(5000);
const withRetries = withTimeout(3);
const apiConfig = withRetries({ 'Authorization': 'Bearer token' });
// Or all at once
const quickConfig = createConfig('https://api.example.com')(5000)(3)({
'Authorization': 'Bearer token'
});
// Use it
apiConfig.request('/users/1').then(user => console.log(user));
const createHandler = action => selector => callback => {
return function(event) {
if (event.target.matches(selector)) {
callback(event);
}
};
};
// Create specialized handlers
const onClick = createHandler('click');
const onSubmit = createHandler('submit');
const onInput = createHandler('input');
// Create element-specific handlers
const onButtonClick = onClick('button');
const onLinkClick = onClick('a');
const onFormSubmit = onSubmit('form');
const onInputChange = onInput('input');
// Use them
document.addEventListener('click', onButtonClick(e => {
console.log('Button clicked:', e.target.textContent);
}));
document.addEventListener('click', onLinkClick(e => {
e.preventDefault();
console.log('Link clicked:', e.target.href);
}));
document.addEventListener('submit', onFormSubmit(e => {
e.preventDefault();
console.log('Form submitted');
}));
Now that you understand currying and partial application, in the next lesson we'll explore function composition - combining simple functions to create complex behavior.