Kodokon kodokon.com

Functional patterns: currying, composition, immutability

Structure your code with currying, function composition, and immutable data to gain predictability and testability.

9 min · 3 questions

Open this lesson in Kodokon

JavaScript treats functions as first-class values, which opens the door to functional patterns. Currying turns a function of n arguments into a chain of single-argument functions: f(a, b, c) becomes f(a)(b)(c). Not to be confused with partial application, which fixes some arguments in a single step (fn.bind(null, a)). Currying shines at creating specialized functions from generic ones.

JAVASCRIPT
const curry = (fn) => {
  const arity = fn.length;
  return function curried(...args) {
    if (args.length >= arity) return fn(...args);
    return (...rest) => curried(...args, ...rest);
  };
};

const add3 = (a, b, c) => a + b + c;
const addCurried = curry(add3);

console.log(addCurried(1)(2)(3)); // 6
console.log(addCurried(1, 2)(3)); // 6
A generic curry based on the function's arity.

A spec subtlety: fn.length only counts the parameters located before the first default or rest parameter. (a, b = 1) => {} has a length of 1, (...args) => {} of 0. An arity-based curry must therefore be given functions with simple parameters. Composition is the next step: assembling small pure functions into readable pipelines.

JAVASCRIPT
const pipe = (...fns) => (input) =>
  fns.reduce((acc, fn) => fn(acc), input);

const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const dashes = (s) => s.replace(/\s+/g, "-");

const toSlug = pipe(trim, lower, dashes);
console.log(toSlug("  Hello World  "));
// "hello-world"
pipe applies the functions from left to right.

pipe(f, g, h)(x) computes h(g(f(x))): you read it in execution order. The compose variant applies right to left, like mathematical notation. These pipelines are only valuable if the functions are pure - no side effects, no hidden dependencies - which calls for immutable data: instead of mutating an object, you produce a new version of it, and reference equality is then enough to detect a change (the heart of React's reconciliation).

JAVASCRIPT
const state = Object.freeze({
  user: { name: "Ada" },
  tags: ["js"]
});

const next = {
  ...state,
  tags: [...state.tags, "fp"]
};

const deepCopy = structuredClone(state);
deepCopy.user.name = "Grace"; // independent copy
A new version through copying rather than mutation.

Knowledge check

Make sure you remember the key points of this lesson.

  1. What is the difference between currying and partial application?
    • None, they are synonyms
    • Currying turns f(a, b, c) into f(a)(b)(c); partial application fixes some arguments in a single call
    • Partial application only works with bind, currying only with arrow functions
    • Currying only applies to two-argument functions
  2. What is pipe(f, g, h)(x) equivalent to?
    • f(g(h(x)))
    • h(g(f(x)))
    • g(f(h(x)))
    • f(x) + g(x) + h(x)
  3. After const s = Object.freeze({ a: { b: 1 } }), does the assignment s.a.b = 2 work?
    • No, freeze freezes the entire object tree
    • No, a TypeError is thrown even outside strict mode
    • Yes, because freeze is shallow: the nested object s.a is not frozen
    • Yes, but only in non-strict mode