Functional Programming Patterns

Difficulty: Advanced

Functional programming (FP) is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing state or mutable data. While JavaScript is a multi-paradigm language, its first-class functions, closures, and expression-based nature make it an excellent host for functional patterns. Understanding FP is increasingly important as React (with hooks), Redux, and many modern libraries are built on functional principles.

The cornerstone of FP is the pure function: a function that always produces the same output for the same input and produces no side effects. Side effects include modifying external variables, writing to the DOM, making network requests, or logging to the console. Pure functions are easy to test (no mocking needed), easy to reason about (no hidden state), safe to memoize (same input guarantees same output), and safe to parallelize. Identifying and isolating side effects from pure logic is a key skill.

Immutability means never modifying data after creation. Instead of mutating an object or array, you create a new copy with the desired changes. JavaScript provides several tools for this: Object.freeze() makes an object shallowly immutable, the spread operator creates shallow copies with modifications, and structuredClone() creates deep copies. Libraries like Immer provide convenient immutable updates through a mutable-looking API. Immutability prevents bugs caused by shared mutable state and makes it easy to implement features like undo/redo and time-travel debugging.

Function composition is the practice of combining simple functions to build more complex ones. The compose() function chains functions right-to-left (mathematical convention), while pipe() chains them left-to-right (more readable). Both take multiple functions and return a new function that passes its input through the chain. For example, pipe(trim, toLowerCase, removeSpaces) creates a function that trims, lowercases, and removes spaces in sequence. This is a powerful way to build data transformation pipelines.

Partial application and currying are related but distinct concepts. Partial application fixes some arguments of a function, returning a new function that takes the remaining arguments. Currying transforms a function that takes multiple arguments into a chain of functions that each take a single argument. While currying always produces unary functions, partial application can fix any number of arguments. Both enable code reuse and point-free style programming.

The concept of monads from category theory appears in JavaScript through Promise. A monad is a design pattern that wraps a value, provides a method to transform the wrapped value (map/then), and supports chaining. Promise.then() takes a function that receives the unwrapped value and returns a new Promise, allowing you to chain transformations. Array is also a monad - flatMap() corresponds to the monadic bind operation. Understanding this connection helps you see the unifying pattern behind Promise chains, array transformations, and optional chaining.

Code examples

Pure functions vs impure functions

// Pure: same input always gives same output, no side effects
function add(a, b) {
  return a + b;
}

function formatName(first, last) {
  return `${first.trim()} ${last.trim()}`;
}

console.log(add(2, 3));           // always 5
console.log(formatName(' Alice ', ' Smith '));

// Impure: depends on external state
let count = 0;
function increment() {
  count += 1;  // side effect: mutates external variable
  return count;
}
console.log(increment()); // 1
console.log(increment()); // 2 - different output for same (no) input

// Impure made pure
function pureIncrement(n) {
  return n + 1;  // no side effects, deterministic
}
console.log(pureIncrement(0)); // always 1
console.log(pureIncrement(0)); // always 1

Pure functions are deterministic and side-effect-free. The impure increment() modifies external state, making it unpredictable. The pure version takes state as input and returns new state.

Immutability patterns

// Object.freeze - shallow immutability
const config = Object.freeze({ host: 'localhost', port: 3000 });
// config.port = 8080; // silently fails (throws in strict mode)
console.log(config.port);

// Spread operator for immutable updates
const user = { name: 'Alice', age: 25, role: 'dev' };
const updated = { ...user, age: 26 };
console.log(user.age, updated.age);
console.log(user === updated);

// Immutable array operations
const nums = [1, 2, 3, 4, 5];
const withSix = [...nums, 6];             // add
const withoutThree = nums.filter(n => n !== 3); // remove
const doubled = nums.map(n => n * 2);     // transform
console.log(nums);          // original unchanged
console.log(withSix);
console.log(withoutThree);
console.log(doubled);

// Deep clone with structuredClone
const nested = { a: { b: { c: 1 } } };
const deep = structuredClone(nested);
deep.a.b.c = 999;
console.log(nested.a.b.c, deep.a.b.c);

Immutable patterns always create new data structures instead of modifying existing ones. Object.freeze prevents mutations, spread creates shallow copies, and structuredClone handles deep nesting.

Function composition: compose and pipe

// compose: right-to-left execution
const compose = (...fns) =>
  (x) => fns.reduceRight((acc, fn) => fn(acc), x);

// pipe: left-to-right execution (more intuitive)
const pipe = (...fns) =>
  (x) => fns.reduce((acc, fn) => fn(acc), x);

const trim = (s) => s.trim();
const toLower = (s) => s.toLowerCase();
const split = (s) => s.split(' ');
const join = (arr) => arr.join('-');

// Build a slug generator using pipe
const slugify = pipe(trim, toLower, split, join);

console.log(slugify('  Hello World  '));
console.log(slugify('  Functional Programming  '));

// Same thing with compose (reversed order)
const slugifyCompose = compose(join, split, toLower, trim);
console.log(slugifyCompose('  Compose Example  '));

pipe() applies functions left-to-right, which reads like a step-by-step transformation. compose() applies right-to-left, matching mathematical notation. Both create reusable transformation pipelines.

Currying and partial application

// Currying: transform f(a, b, c) into f(a)(b)(c)
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...nextArgs) => curried(...args, ...nextArgs);
  };
}

const multiply = curry((a, b, c) => a * b * c);
console.log(multiply(2)(3)(4));     // fully curried
console.log(multiply(2, 3)(4));     // partial + curried
console.log(multiply(2, 3, 4));     // all at once

// Practical use: creating specialized functions
const add = curry((a, b) => a + b);
const add10 = add(10);
console.log([1, 2, 3].map(add10));

// Partial application (without full currying)
function partial(fn, ...presetArgs) {
  return (...laterArgs) => fn(...presetArgs, ...laterArgs);
}

const greet = (greeting, name) => `${greeting}, ${name}!`;
const sayHello = partial(greet, 'Hello');
const sayGoodbye = partial(greet, 'Goodbye');
console.log(sayHello('Alice'));
console.log(sayGoodbye('Bob'));

Currying converts a multi-argument function into a chain of single-argument functions. Partial application fixes some arguments upfront. Both create specialized, reusable function variants.

Key points

Concepts covered

Pure Functions, Immutability, Composition, Higher-Order Functions, Currying, Partial Application, Monads