Lesson 2 of 8

Pure Functions

Understanding pure functions, side effects, and referential transparency

What is a Pure Function?

A pure function is a function that:

  1. Deterministic: Given the same input, always returns the same output
  2. No Side Effects: Does not modify any external state or depend on it

Pure functions are the building blocks of functional programming. They're predictable, testable, and can be safely reused anywhere.

✅ Pure Function Examples

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

function double(x) {
  return x * 2;
}

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

// Array methods that return new arrays are pure
function getActiveUsers(users) {
  return users.filter(user => user.active);
}

// String methods are pure
function toUpperCase(str) {
  return str.toUpperCase();
}

❌ Impure Function Examples

// Impure: depends on external state
let taxRate = 0.1;
function calculateTax(price) {
  return price * taxRate; // Uses external variable
}

// Impure: modifies external state
let total = 0;
function addToTotal(amount) {
  total += amount; // Mutates external variable
  return total;
}

// Impure: modifies input
function addItem(cart, item) {
  cart.push(item); // Mutates the input array
  return cart;
}

// Impure: I/O operations
function logMessage(msg) {
  console.log(msg); // Side effect: writes to console
}

// Impure: random/time-based
function getRandomNumber() {
  return Math.random(); // Different output each time
}

function getCurrentTime() {
  return new Date(); // Depends on current time
}

Side Effects

A side effect is any change in state that is observable outside the function, or any dependency on external state.

// Common side effects:

// 1. Mutating input parameters
function sortArray(arr) {
  return arr.sort(); // ❌ Mutates original array!
}

// Pure version:
function sortArrayPure(arr) {
  return [...arr].sort(); // ✅ Creates new array
}

// 2. Modifying global/external variables
let counter = 0;
function increment() {
  counter++; // ❌ Modifies external state
}

// 3. DOM manipulation
function updateUI(message) {
  document.getElementById('output').textContent = message; // ❌
}

// 4. Network requests
async function fetchUser(id) {
  return await fetch(`/api/users/${id}`); // ❌ I/O side effect
}

// 5. Writing to storage
function saveData(data) {
  localStorage.setItem('data', JSON.stringify(data)); // ❌
}

// 6. Logging
function processData(data) {
  console.log('Processing...', data); // ❌ Side effect
  return data.map(x => x * 2);
}

Referential Transparency

A function is referentially transparent if it can be replaced with its return value without changing the program's behavior.

// Referentially transparent
function add(a, b) {
  return a + b;
}

// This expression:
const result = add(2, 3) + add(2, 3);

// Can be replaced with:
const result = 5 + 5;

// The program behaves identically!

// NOT referentially transparent
let count = 0;
function incrementAndGet() {
  return ++count;
}

// This expression:
const x = incrementAndGet() + incrementAndGet();
// x = 1 + 2 = 3

// Cannot be replaced with:
const x = 1 + 1; // Would give 2, not 3!

// The function returns different values each time

Making Impure Functions Pure

// BEFORE: Impure - uses external state
let discount = 0.1;
function applyDiscount(price) {
  return price * (1 - discount);
}

// AFTER: Pure - all dependencies are parameters
function applyDiscount(price, discount) {
  return price * (1 - discount);
}

// BEFORE: Impure - mutates input
function addTodo(todos, text) {
  todos.push({ text, done: false });
  return todos;
}

// AFTER: Pure - returns new array
function addTodo(todos, text) {
  return [...todos, { text, done: false }];
}

// BEFORE: Impure - depends on Date
function isExpired(expirationDate) {
  return new Date() > expirationDate;
}

// AFTER: Pure - inject current time
function isExpired(expirationDate, currentDate) {
  return currentDate > expirationDate;
}
// Or use dependency injection
function createIsExpired(getCurrentDate) {
  return (expirationDate) => getCurrentDate() > expirationDate;
}

Benefits of Pure Functions

// 1. TESTABLE - No mocking needed
function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// Test is simple:
test('calculateTotal sums prices', () => {
  const items = [{ price: 10 }, { price: 20 }];
  expect(calculateTotal(items)).toBe(30);
});

// 2. CACHEABLE - Same input = same output
const memoize = (fn) => {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
};

const expensiveCalculation = memoize((n) => {
  console.log('Computing...');
  return n * n;
});

expensiveCalculation(5); // Computing... 25
expensiveCalculation(5); // 25 (cached, no computation)

// 3. PARALLELIZABLE - No shared state to worry about
const results = await Promise.all([
  pureFunction(data1),
  pureFunction(data2),
  pureFunction(data3),
]);

// 4. COMPOSABLE - Easy to combine
const processData = compose(
  formatOutput,
  filterInvalid,
  transformData,
  parseInput
);

📖 Wikipedia: Pure Functions →

Handling Side Effects

// Strategy 1: Push side effects to the edges
// Keep core logic pure, handle effects at boundaries

// Pure core logic
function calculateOrder(items, discount) {
  const subtotal = items.reduce((sum, i) => sum + i.price, 0);
  const discountAmount = subtotal * discount;
  return {
    subtotal,
    discount: discountAmount,
    total: subtotal - discountAmount,
  };
}

// Side effects at the boundary
async function processOrder(orderId) {
  // Side effect: fetch data
  const items = await fetchItems(orderId);
  const discount = await fetchDiscount(orderId);
  
  // Pure calculation
  const order = calculateOrder(items, discount);
  
  // Side effect: save result
  await saveOrder(orderId, order);
  
  // Side effect: notify
  await sendConfirmation(orderId);
  
  return order;
}

// Strategy 2: Return descriptions of effects (like Redux)
function reducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return { ...state, todos: [...state.todos, action.todo] };
    // Pure function returns new state
    // Side effects happen elsewhere (middleware)
  }
}

💡 Pure Function Checklist

  • ✅ Does it only use its parameters?
  • ✅ Does it return a value based only on inputs?
  • ✅ Does it avoid modifying parameters?
  • ✅ Does it avoid reading/writing external variables?
  • ✅ Does it avoid I/O (console, network, storage)?
  • ✅ Does it avoid random values or current time?