Skip to main content

🧮 Functional Programming Concepts

Functional programming is a style, not a language feature: build software out of small pure functions, avoid changing data in place, and compose behavior from predictable pieces. JavaScript supports it beautifully, and React and Redux are built on its ideas.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what a pure function is and why purity aids testing and debugging
  • Apply immutability using spread syntax instead of mutating data
  • Contrast declarative functional code with imperative loops
  • Use composition, avoid shared state, and understand the Maybe functor
  • Recognize functional principles in React and Redux reducers

Estimated Time: 40–50 minutes  •  Difficulty: Intermediate

Hands-on: Refactor an imperative data routine into a pure, composed pipeline.

In This Lesson

What Is Functional Programming?

Functional programming (FP) is a paradigm that treats computation as the evaluation of functions and avoids changing state or mutating data. Where imperative code describes how to do something step by step, functional code describes what you want as a series of transformations.

🧮 A kitchen analogy: Functional programming is like a recipe that never alters the original ingredients. You take inputs, apply steps that each produce a new result, and end with a finished dish — the pantry is exactly as you found it. Imperative code, by contrast, keeps reaching back into the pantry and rearranging it as it goes.
graph TD A[Functional Programming] --> B[Pure Functions] A --> C[Immutability] A --> D[First-Class Functions] A --> E[Composition] A --> F[Declarative Style]

The same task in both styles — summing the even numbers — shows the difference. Both return 30, but one narrates the steps and the other declares the intent:

// Imperative: describe HOW, mutate a running total
function sumEvensImperative(numbers) {
  let sum = 0;
  for (let i = 0; i < numbers.length; i++) {
    if (numbers[i] % 2 === 0) sum += numbers[i];
  }
  return sum;
}

// Functional: describe WHAT, compose transformations
function sumEvensFunctional(numbers) {
  return numbers
    .filter((n) => n % 2 === 0)
    .reduce((sum, n) => sum + n, 0);
}

const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(sumEvensFunctional(nums)); // 30

Pure Functions

A pure function obeys two rules: given the same input it always returns the same output, and it produces no side effects (it doesn't modify anything outside itself — no globals, no mutating arguments, no I/O). Purity is the single most important idea in FP because it makes code predictable.

// Impure: reads and mutates external state
let counter = 0;
function increment() {
  counter += 1; // side effect
  return counter;
}
increment(); // 1
increment(); // 2  — same call, different result

// Pure: output depends only on inputs
function add(a, b) {
  return a + b;
}
add(2, 3); // 5, forever

// Impure: mutates the argument
function addItemImpure(arr, item) {
  arr.push(item); // caller's array is changed
  return arr;
}

// Pure: returns a new array, leaves the input alone
function addItemPure(arr, item) {
  return [...arr, item];
}

✅ Why purity is worth it

  • Testable: no setup or mocking — just assert output for input.
  • Debuggable: a bug is reproducible from its inputs alone.
  • Cacheable: pure functions are safe to memoize.
  • Parallel-safe: no shared mutable state means no race conditions.

Immutability

Immutability means never changing data after you create it. Instead of editing an object or array in place, you produce a new copy with the change applied. This prevents a whole category of "who changed my data?" bugs and makes state transitions easy to trace.

// Mutable — the original cart is altered
function addToCartMutable(cart, item) {
  cart.items.push(item);
  cart.total += item.price;
  return cart;
}

// Immutable — a brand new cart object is returned
function addToCartImmutable(cart, item) {
  return {
    ...cart,
    items: [...cart.items, item],
    total: cart.total + item.price,
  };
}

const cart = { items: [], total: 0 };
const updated = addToCartImmutable(cart, { name: 'Book', price: 20 });

console.log(cart.total);    // 0  — original untouched
console.log(updated.total); // 20 — new object
Mutation versus immutable update Mutation changes the original object in place, so old references see the change. An immutable update leaves the original intact and returns a new object. Mutation original (edited in place) same object, now changed Immutable update original (untouched) new copy with the change
Figure 1 — Immutable updates keep the original safe, which is exactly why React and Redux can compare references to detect change.

⚠️ Spread is a shallow copy

{ ...obj } and [...arr] copy only one level deep. Nested objects are still shared references. For deep updates, spread at each level you change, or use a helper like Immer that lets you write "mutating" code that produces immutable results.

Composition & No Shared State

Functional code builds big behavior by composing small functions, and it keeps functions independent by avoiding shared mutable state. Compose lets one function's output flow into the next; isolation via closures gives each unit its own state.

const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

const double = (x) => x * 2;
const increment = (x) => x + 1;
const square = (x) => x * x;

const transform = pipe(double, increment, square);
console.log(transform(3)); // 49  (3 → 6 → 7 → 49)

Shared mutable state is the enemy of predictability: functions that read and write the same variable become order-dependent and hard to reason about. Encapsulate state in a closure instead so each instance is independent:

// Problematic: everything mutates one shared variable
let sharedTotal = 0;
const addToTotal = (v) => (sharedTotal += v); // order-dependent, fragile

// Better: isolated state per counter
function createCounter(start = 0) {
  let count = start;
  return {
    increment: (v = 1) => (count += v),
    value: () => count,
  };
}

const a = createCounter();
const b = createCounter(100);
a.increment(10); // 10
b.increment(10); // 110 — completely independent

Currying & the Maybe Functor

Two techniques appear constantly in functional codebases.

Currying

Currying turns a multi-argument function into a chain of single-argument functions, which makes partial application natural. Arrow syntax makes it concise:

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

// Fix the first argument to build a specialized function
const add10 = add(10);
console.log(add10(5)(1)); // 16

The Maybe functor

A functor is a container with a map method that applies a function to the value inside while preserving the container. The Maybe functor models a value that might be missing, letting you chain transformations without a pile of null checks:

class Maybe {
  constructor(value) { this._value = value; }
  static of(value) { return new Maybe(value); }
  isNothing() { return this._value == null; } // null or undefined
  map(fn) {
    return this.isNothing() ? this : Maybe.of(fn(this._value));
  }
  getOrElse(fallback) {
    return this.isNothing() ? fallback : this._value;
  }
}

const getName = (person) => person.name;
const upper = (s) => s.toUpperCase();

// Safe even when the value is null — no crash
console.log(Maybe.of(null).map(getName).map(upper).getOrElse('No name')); // "No name"
console.log(Maybe.of({ name: 'Alice' }).map(getName).map(upper).getOrElse('No name')); // "ALICE"

💡 You already know a functor

An array is a functor: [1,2,3].map(x => x*2) applies a function to each value and hands back a new array. Maybe is the same idea for a container that holds zero-or-one values. Libraries like Ramda lean on these abstractions heavily.

FP in React & Redux

You do not have to seek functional programming out — the tools you will use apply it for you.

React: pure rendering, immutable updates

A React component is conceptually a pure function of its props and state, and state updates are done immutably. Notice every handler returns new data rather than mutating the old:

function useTodos() {
  const [todos, setTodos] = useState([]);

  const addTodo = (text) =>
    setTodos((prev) => [...prev, { id: Date.now(), text, done: false }]);

  const toggle = (id) =>
    setTodos((prev) =>
      prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
    );

  const remove = (id) =>
    setTodos((prev) => prev.filter((t) => t.id !== id));

  return { todos, addTodo, toggle, remove };
}

Redux: reducers are pure functions

A Redux reducer is a pure function (state, action) => newState. It never mutates the existing state; it returns a new object. That purity is what powers time-travel debugging and predictable updates.

const initialState = { todos: [] };

function todosReducer(state = initialState, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return { ...state, todos: [...state.todos, action.payload] };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((t) =>
          t.id === action.payload.id ? { ...t, done: !t.done } : t
        ),
      };
    default:
      return state;
  }
}

📖 Definition

Referential transparency: an expression can be replaced by its value without changing the program's behavior. Pure functions are referentially transparent — the property that makes functional code so easy to reason about.

Hands-on Exercise

🏋️ Refactor Imperative Code into a Pure Pipeline

Objective: Turn a step-by-step routine into pure, composed functions.

Starting point:

const sales = [
  { product: 'Laptop', price: 1200, qty: 5 },
  { product: 'Phone', price: 800, qty: 10 },
  { product: 'Mouse', price: 50, qty: 25 },
];

Instructions:

  1. Write a pure function withValue(sale) that returns a new object with an added value = price * qty field (do not mutate the input).
  2. Use map, filter, and reduce to compute the total value of sales worth more than 1000.
  3. Assemble the steps with pipe so the whole thing reads top to bottom.
  4. Confirm the original sales array is unchanged afterwards.
💡 Hint

Build small pure stages: addValues = (arr) => arr.map(withValue), bigOnes = (arr) => arr.filter(s => s.value > 1000), sumValues = (arr) => arr.reduce((t, s) => t + s.value, 0). Then pipe(addValues, bigOnes, sumValues).

✅ Sample solution
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

const withValue = (sale) => ({ ...sale, value: sale.price * sale.qty });

const addValues = (arr) => arr.map(withValue);
const bigOnes   = (arr) => arr.filter((s) => s.value > 1000);
const sumValues = (arr) => arr.reduce((t, s) => t + s.value, 0);

const totalBigSales = pipe(addValues, bigOnes, sumValues);

console.log(totalBigSales(sales)); // 6000 + 8000 = 14000
console.log(sales[0].value);       // undefined — originals untouched

Every stage is pure, the input is never mutated, and the pipeline reads as a clear sequence of transformations.

Best Practices

✅ Do

  • Default to pure functions; push side effects (I/O, DOM, network) to the edges of your program.
  • Use spread syntax to update data immutably, especially in React and Redux state.
  • Compose small, well-named functions rather than writing one large procedure.
  • Mix paradigms pragmatically — imperative code is fine for orchestration and quick loops.

⚠️ Don't

  • Don't mutate function arguments or shared state — it breaks predictability.
  • Don't forget spread is shallow; nested structures need care or a helper like Immer.
  • Don't force a purely functional style everywhere if it hurts readability.
  • Don't hide side effects inside functions that look pure — name them honestly.

Summary & Quiz

🎉 Key Takeaways

  • Pure functions — same input, same output, no side effects — are the core of FP.
  • Immutability means creating new data instead of mutating existing data.
  • Functional code is declarative: it says what to compute, not how to loop.
  • Composition and avoiding shared state keep code modular and predictable.
  • React and Redux reducers are functional programming in daily practice.

🎯 Quick Quiz

Question 1: Which pair of rules makes a function "pure"?

Question 2: Why do React and Redux rely on immutable updates?

Question 3: What does Maybe.of(null).map(fn).getOrElse('x') return?

📚 Further Reading

🚀 What's Next?

You've now covered the advanced-JavaScript trio of closures, higher-order functions, and functional style. Next the module shifts to resilience: the different types of errors in JavaScript and how to handle them.

🎉 Well done!

You can now write predictable, composable, immutable JavaScript. On to handling the things that go wrong.