Advanced JavaScriptFeatured

JavaScript Currying: Functional Programming Technique

Master currying in JavaScript. Learn how to transform functions, create reusable utilities, and apply functional programming patterns.

By JavaScriptDoc Team
curryingfunctional programminghigher-order functionspartial applicationcomposition

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!