🚀AdvancedAdvanced Patterns

Functional Programming

Apply functional programming principles pure functions, immutability, currying, function composition, and functors to write predictable, testable JavaScript.

Overview

Functional programming (FP) is a paradigm that treats computation as the evaluation of mathematical functions. The two core tenets are pure functions (a function that always returns the same output for the same input and has no side effects) and immutability (data is never mutated; instead, new data is derived from old data). These constraints sound restrictive but in practice they make programs dramatically easier to reason about, test, and parallelise. JavaScript supports FP naturally: functions are first-class values, closures capture scope, and the standard library provides map, filter, reduce, and flat. Beyond those basics, FP in JavaScript means currying (transforming a multi-argument function into a chain of single-argument functions), partial application (fixing some arguments ahead of time), function composition (building pipelines where the output of one function feeds the next), and point-free style (expressing transformations without naming intermediate values). Advanced FP concepts like functors (a value in a container that supports map), monads (a functor with flatMap for sequencing computations), and transducers (composable, efficient transformations) appear in production codebases through libraries like Ramda, fp-ts, and RxJS. Even if you never use those libraries, internalising FP principles makes your code more modular: each function does one thing, takes data in, returns data out, and has no hidden dependencies.

Key Points

  • •Pure functions: same input always produces same output; no side effects (no mutation, I/O, or global state)
  • •Immutability: use Object.freeze, spread ({...obj}), and Array methods that return new arrays instead of mutating
  • •Currying transforms f(a, b) into f(a)(b) enables partial application and reusable specialised functions
  • •Partial application fixes some arguments upfront; the resulting function takes the remaining arguments later
  • •Function composition (compose/pipe) chains functions left-to-right or right-to-left into a pipeline
  • •A functor is anything with a map method that preserves structure Array, Maybe, and Promise are all functors
  • •Transducers compose transformations without building intermediate arrays, processing data in a single pass
  • •Point-free style: define functions as compositions without referencing the data argument explicitly

Examples

Currying and partial application

// Manual curry
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return function(...more) {
      return curried.apply(this, args.concat(more));
    };
  };
}

const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3));   // 6
console.log(add(1, 2)(3));   // 6
console.log(add(1)(2, 3));   // 6

const add10 = add(10);       // partial application
const add10and5 = add10(5);
console.log(add10and5(2));   // 17

// Practical use: curried filter predicate
const isGreaterThan = curry((threshold, value) => value > threshold);
const isAdult = isGreaterThan(17);

const ages = [14, 22, 16, 25, 13, 18];
console.log(ages.filter(isAdult)); // [22, 25, 18]

Curried functions are composable you build specialised functions from general ones by partial application.

Function composition and pipe

// Right-to-left (mathematical) composition
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);

// Left-to-right (pipeline) composition
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

const double    = x => x * 2;
const addOne    = x => x + 1;
const square    = x => x * x;
const toStr     = x => `Result: ${x}`;

// compose applies right-to-left: square(addOne(double(3)))
const transform1 = compose(toStr, square, addOne, double);
console.log(transform1(3)); // Result: 49  (3*2=6, 6+1=7, 7^2=49)

// pipe applies left-to-right: same functions, same result
const transform2 = pipe(double, addOne, square, toStr);
console.log(transform2(3)); // Result: 49

// Real-world use: data pipeline
const processUsers = pipe(
  users => users.filter(u => u.active),
  users => users.map(u => ({ ...u, name: u.name.trim().toUpperCase() })),
  users => users.sort((a, b) => a.name.localeCompare(b.name)),
);

const users = [
  { name: '  alice ', active: true },
  { name: 'BOB',      active: false },
  { name: 'charlie ', active: true },
];
console.log(processUsers(users));
// [{ name: 'ALICE', active: true }, { name: 'CHARLIE', active: true }]

pipe reads in execution order (top-to-bottom) which most developers find more intuitive than compose.

Maybe functor safe null handling without if-chains

class Maybe {
  constructor(value) { this._value = value; }

  static of(value) { return new Maybe(value); }
  static empty()   { return new Maybe(null); }

  isNothing() { return this._value == null; }

  map(fn) {
    return this.isNothing() ? Maybe.empty() : Maybe.of(fn(this._value));
  }

  getOrElse(defaultValue) {
    return this.isNothing() ? defaultValue : this._value;
  }

  toString() {
    return this.isNothing() ? 'Nothing' : `Just(${this._value})`;
  }
}

// Safe property access chain
const getCity = user =>
  Maybe.of(user)
    .map(u => u.address)
    .map(a => a.city)
    .map(c => c.toUpperCase())
    .getOrElse('UNKNOWN');

console.log(getCity({ address: { city: 'London' } })); // LONDON
console.log(getCity({ address: null }));               // UNKNOWN
console.log(getCity(null));                            // UNKNOWN
console.log(getCity({}));                              // UNKNOWN

Maybe is a functor because it implements map that preserves the Maybe structure. It safely propagates null through a chain without if/else at every step.

Common Mistakes

  • Calling array methods that mutate (sort, reverse, splice, push) in a "functional" pipeline always copy first: [...arr].sort()
  • Writing impure functions that read from or write to variables outside their scope, making them untestable in isolation
  • Over-abstracting with point-free style when a simple named function would be clearer FP is a tool, not a dogma
  • Accidentally sharing mutable objects by spreading only shallowly nested objects are still the same reference after {...obj}

Note

Start applying FP incrementally: make your utility functions pure first, then introduce currying where you find yourself creating many similar filter/map callbacks. You do not need to adopt full monadic style to benefit from FP even 10% FP discipline (pure utils, immutable updates) pays dividends in testability.

Exercises

Write JavaScript code to solve each exercise. Run it and check your output.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What makes a function "pure"?