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:
- Deterministic: Given the same input, always returns the same output
- 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
);
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?