Currying and Partial Application in JavaScript

What is Currying?

Currying is a functional programming technique that transforms a function with multiple arguments into a sequence of functions, each taking a single argument. The curried function returns a new function for each argument until all arguments have been provided, at which point the original function is executed.

Basic Currying Example

// Regular function with multiple arguments
function add(a, b, c) {
  return a + b + c;
}

// Curried version
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}

// Usage
console.log(add(1, 2, 3)); // 6
console.log(curriedAdd(1)(2)(3)); // 6

// With arrow functions (more concise)
const curriedAddArrow = a => b => c => a + b + c;
console.log(curriedAddArrow(1)(2)(3)); // 6

Creating a Curry Function

// Generic curry 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));
      };
    }
  };
}

// Usage
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

What is Partial Application?

Partial application is a technique that fixes a number of arguments to a function, producing another function of smaller arity (fewer parameters). Unlike currying, partial application takes multiple arguments at once and returns a function that takes the remaining arguments.

Partial Application Examples

// Manual partial application
function multiply(a, b, c) {
  return a * b * c;
}

function partialMultiply(a, b) {
  return function(c) {
    return multiply(a, b, c);
  };
}

const multiplyBy10And2 = partialMultiply(10, 2);
console.log(multiplyBy10And2(5)); // 10 * 2 * 5 = 100

// Using bind for partial application
const multiply10 = multiply.bind(null, 10);
console.log(multiply10(2, 5)); // 10 * 2 * 5 = 100

const multiply10And2 = multiply.bind(null, 10, 2);
console.log(multiply10And2(5)); // 10 * 2 * 5 = 100

Creating a Partial Application Function

function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  };
}

// Usage
function greet(greeting, name, punctuation) {
  return `${greeting}, ${name}${punctuation}`;
}

const greetWithHello = partial(greet, "Hello");
console.log(greetWithHello("John", "!")); // "Hello, John!"

const sayHelloToJohn = partial(greet, "Hello", "John");
console.log(sayHelloToJohn("!")); // "Hello, John!"

Differences Between Currying and Partial Application

CurryingPartial Application
Transforms a function with n arguments into n functions with a single argumentFixes a number of arguments and returns a function that takes the remaining arguments
Returns a new function for each argumentReturns a single function for all remaining arguments
All arguments must be specified one by oneMultiple arguments can be specified at once
add(1)(2)(3)partialAdd(1, 2)(3)

Practical Applications

1. Function Composition

const double = x => x * 2;
const increment = x => x + 1;

// Compose functions
const compose = (f, g) => x => f(g(x));
const doubleAndIncrement = compose(increment, double);

console.log(doubleAndIncrement(5)); // (5 * 2) + 1 = 11

2. Event Handling

// Without currying
function handleClick(id, event) {
  console.log(`Element ${id} clicked at position:`, event.clientX, event.clientY);
}

// With currying
const handleClickCurried = id => event => {
  console.log(`Element ${id} clicked at position:`, event.clientX, event.clientY);
};

document.getElementById('button1').addEventListener('click', event => handleClick('button1', event));
document.getElementById('button2').addEventListener('click', handleClickCurried('button2'));

3. Configurable Functions

// Curried API request function
const apiRequest = baseUrl => endpoint => params => {
  const url = `${baseUrl}${endpoint}?${new URLSearchParams(params)}`;
  return fetch(url).then(res => res.json());
};

// Create specialized functions
const githubApi = apiRequest('https://api.github.com');
const githubUsers = githubApi('/users');
const githubRepos = githubApi('/repositories');

// Use the specialized functions
githubUsers({ since: 10 }).then(users => console.log(users));
githubRepos({ since: 'daily' }).then(repos => console.log(repos));

4. Memoization

function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (!(key in cache)) {
      cache[key] = fn.apply(this, args);
    }
    return cache[key];
  };
}

// Usage with currying
const memoizedAdd = memoize(curriedAdd);
console.log(memoizedAdd(1)(2)(3)); // Calculated
console.log(memoizedAdd(1)(2)(3)); // Retrieved from cache

Advanced Currying Patterns

1. Placeholder Currying

// Placeholder implementation
const _ = Symbol('placeholder');

function advancedCurry(fn) {
  const arity = fn.length;
  
  return function curried(...args) {
    const filledArgs = args.map(arg => arg === _ ? undefined : arg);
    const hasPlaceholders = filledArgs.some(arg => arg === undefined);
    const effectiveArity = arity - filledArgs.filter(arg => arg !== undefined).length;
    
    if (!hasPlaceholders && effectiveArity <= 0) {
      return fn.apply(this, filledArgs);
    }
    
    return function(...nextArgs) {
      const newArgs = [...filledArgs];
      let nextArgIndex = 0;
      
      for (let i = 0; i < newArgs.length && nextArgIndex < nextArgs.length; i++) {
        if (newArgs[i] === undefined) {
          newArgs[i] = nextArgs[nextArgIndex++];
        }
      }
      
      while (nextArgIndex < nextArgs.length) {
        newArgs.push(nextArgs[nextArgIndex++]);
      }
      
      return curried.apply(this, newArgs);
    };
  };
}

// Usage
const curriedFn = advancedCurry((a, b, c, d) => a + b + c + d);
console.log(curriedFn(1, _, 3, _)(2, 4)); // 1 + 2 + 3 + 4 = 10

2. Auto-Currying

// Convert any function to a curried version automatically
function autoCurry(fn, arity = fn.length) {
  return function curried(...args) {
    if (args.length >= arity) {
      return fn.apply(this, args);
    }
    return autoCurry(
      (...moreArgs) => fn.apply(this, [...args, ...moreArgs]),
      arity - args.length
    );
  };
}

// Usage
const autoSum = autoCurry((a, b, c, d) => a + b + c + d);
console.log(autoSum(1)(2)(3)(4)); // 10
console.log(autoSum(1, 2)(3, 4)); // 10

Best Practices

  1. Use currying for function specialization and creating reusable function templates
  2. Use partial application when order matters and you want to pre-fill specific arguments
  3. Consider performance implications as excessive currying can create many function objects
  4. Use libraries like Lodash or Ramda for production-ready currying and partial application
  5. Document curried functions clearly as the calling pattern may not be obvious

Interview Tips

  • Explain the difference between currying and partial application with examples
  • Describe scenarios where currying or partial application would be beneficial
  • Demonstrate how to implement a curry function from scratch
  • Discuss how currying relates to function composition and point-free programming
  • Explain the performance and readability trade-offs of curried functions

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.