Pure Functions in Functional Programming
Understanding and writing pure functions for predictable, testable code
Introduction
Pure functions are the building blocks of functional programming. They're predictable, testable, and composable. Understanding purity is essential for writing reliable code.
What Makes a Function Pure?
A function is pure if it satisfies two conditions:
1. Deterministic Output
Given the same input, a pure function always returns the same output.
// ✅ Pure - always returns the same result
const add = (a, b) => a + b;
add(2, 3); // Always 5
// ❌ Impure - different results each time
const getRandom = () => Math.random();
getRandom(); // Different every time
2. No Side Effects
A pure function doesn't modify anything outside its scope.
// ✅ Pure - no side effects
const multiply = (a, b) => a * b;
// ❌ Impure - modifies external state
let counter = 0;
const increment = () => {
counter++; // Side effect!
return counter;
};
Examples of Pure Functions
// Mathematical operations
const square = (x) => x * x;
const sum = (a, b) => a + b;
// String operations
const toUpperCase = (str) => str.toUpperCase();
const reverse = (str) => str.split('').reverse().join('');
// Array operations (immutable)
const doubleAll = (arr) => arr.map((n) => n * 2);
const filterEven = (arr) => arr.filter((n) => n % 2 === 0);
Examples of Impure Functions
DOM Access
// ❌ Impure - accesses external state
const getBodyText = () => document.body.innerText;
HTTP Requests
// ❌ Impure - network call
const fetchUser = async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
File I/O
// ❌ Impure - file system access
const readFile = (path) => fs.readFileSync(path, 'utf8');
Random Numbers
// ❌ Impure - non-deterministic
const rollDice = () => Math.floor(Math.random() * 6) + 1;
Date/Time
// ❌ Impure - changes over time
const now = () => new Date();
Console Output
// ❌ Impure - side effect
const log = (msg) => console.log(msg);
Why Pure Functions Matter
Testability
Pure functions are trivial to test:
// Easy to test - no setup needed
test('add returns sum', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
});
Predictability
You can reason about pure functions in isolation:
// No hidden dependencies
const result = calculateTax(income, rate);
// You know exactly what affects the result
Memoization
Pure functions can be cached:
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) => {
// Pure function that can be cached
});
Parallelization
Pure functions can run in parallel safely:
// No shared state = safe to parallelize
const results = await Promise.all(items.map((item) => pureTransform(item)));
Composability
Pure functions compose beautifully:
const process = (data) => pipe(cleanData, transformData, validateData)(data);
Dealing with Impurity
Real applications need side effects. The key is to isolate them.
Pattern 1: Separate Pure and Impure Logic
// ❌ Mixed concerns
const processUser = async (userData) => {
const validated = validate(userData); // Pure
await db.save(validated); // Impure
return validated;
};
// ✅ Separated
const validateUser = (userData) => {
/* Pure */
};
const saveUser = async (user) => {
/* Impure */
};
// Compose at the edge
const processUser = async (userData) => {
const validated = validateUser(userData);
return await saveUser(validated);
};
Pattern 2: Dependency Injection
// Pure function that takes impure dependencies
const createUser = (dependencies) => async (userData) => {
const validated = validateUser(userData);
return await dependencies.save(validated);
};
// Inject impure dependencies at the edge
const createUserWithDB = createUser({ save: db.save });
Pattern 3: Effect Handlers
// Describe effects, execute separately
const program = () => ({
type: 'FETCH_USER',
userId: 123,
});
// Handler executes the effect
const handlers = {
FETCH_USER: async ({ userId }) => {
return await fetch(`/api/users/${userId}`);
},
};
Common Misconceptions
"const" Doesn't Make Functions Pure
// ❌ Still impure
const obj = { value: 0 };
const increment = () => {
obj.value++; // Mutation!
};
Pure Functions Can't Use External Constants
// ✅ Still pure if the constant is immutable
const PI = 3.14159;
const circleArea = (radius) => PI * radius * radius;
All Functional Code Must Be Pure
// ❌ Impossible - apps need side effects
// ✅ Isolate side effects, maximize purity
Practical Guidelines
Maximize Pure Functions
Write as much logic as possible with pure functions. Push side effects to the boundaries.
Immutability Helps
Use immutable data structures to maintain purity:
// ✅ Pure with spread
const addItem = (arr, item) => [...arr, item];
// ❌ Impure with mutation
const addItemImpure = (arr, item) => {
arr.push(item);
return arr;
};
Document Side Effects
When you must use impure functions, make it obvious:
// Clear from the name
const saveToDatabase = async (user) => {
/* ... */
};
const logEvent = (event) => {
/* ... */
};
Conclusion
Pure functions are your foundation for reliable code. While you can't eliminate all side effects, you can minimize them and isolate them at the boundaries. The result is code that's easier to test, debug, and reason about.
#functional-programming #javascript #clean-code