Learning Objectives

  • Understand what currying is and why it's useful
  • Learn partial application techniques
  • Create curried functions using closures
  • Apply these patterns to real-world problems

What Is Currying?

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

Why Curry?

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

Practical Example: Logging

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

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?"

Practical Example: API Requests

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

Auto-Currying Function

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

Practical Example: Validation

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

Practical Example: Configuration Builder

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));

Practical Example: Event Handler Factory

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');
}));

Benefits of Currying and Partial Application

  • Reusability: Create specialized functions from generic ones
  • Composition: Easier to combine functions
  • Configuration: Pre-configure functions with common arguments
  • Readability: More expressive code
  • Flexibility: Apply arguments incrementally

Key Takeaways

  • Currying transforms multi-argument functions into single-argument chains
  • Partial application fixes some arguments, leaving others open
  • ✅ Both techniques use closures to remember arguments
  • ✅ Create specialized functions from generic ones
  • ✅ Improves code reusability and composition

Next Steps

Now that you understand currying and partial application, in the next lesson we'll explore function composition - combining simple functions to create complex behavior.