Docs
5.9-Pure-Functions-Side-Effects
5.9 Pure Functions and Side Effects
Overview
Pure functions are functions that always produce the same output for the same input and have no side effects. Understanding pure functions is fundamental to functional programming and writing predictable, testable, and maintainable code.
Table of Contents
- ā¢What is a Pure Function?
- ā¢Characteristics of Pure Functions
- ā¢Side Effects Explained
- ā¢Pure vs Impure Examples
- ā¢Benefits of Pure Functions
- ā¢Common Side Effects
- ā¢Managing Side Effects
- ā¢Immutability
- ā¢Referential Transparency
- ā¢Best Practices
What is a Pure Function?
A pure function:
- ā¢Always returns the same output for the same input
- ā¢Has no side effects (doesn't modify external state)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā PURE FUNCTION ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā INPUT āāāāāāāŗ PURE FUNCTION āāāāāāāŗ OUTPUT ā
ā ā ā
ā ā ā
ā No external ā
ā interactions ā
ā ā
ā Same input ALWAYS produces same output ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Simple Example
// ā
Pure function
function add(a, b) {
return a + b;
}
add(2, 3); // Always 5
add(2, 3); // Always 5
// ā Impure function
let total = 0;
function addToTotal(value) {
total += value; // Modifies external state
return total;
}
addToTotal(5); // Returns 5
addToTotal(5); // Returns 10 (different output!)
Characteristics of Pure Functions
1. Deterministic
Same inputs always produce same outputs.
// ā
Deterministic
function multiply(a, b) {
return a * b;
}
// Always predictable
multiply(4, 5); // 20
multiply(4, 5); // 20
multiply(4, 5); // 20
// ā Non-deterministic
function randomMultiply(a) {
return a * Math.random();
}
randomMultiply(5); // 2.3456...
randomMultiply(5); // 4.1234... (different!)
2. No Side Effects
Doesn't modify anything outside its scope.
// ā
No side effects
function formatName(firstName, lastName) {
return `${firstName} ${lastName}`;
}
// ā Has side effects
let greeting;
function setGreeting(name) {
greeting = `Hello, ${name}`; // Modifies external variable
}
3. Depends Only on Input
Doesn't use external mutable state.
// ā
Depends only on input
function calculateTax(amount, rate) {
return amount * rate;
}
// ā Depends on external state
const taxRate = 0.2;
function calculateTaxImpure(amount) {
return amount * taxRate; // Uses external variable
}
Side Effects Explained
A side effect is any interaction with the outside world.
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā SIDE EFFECTS ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā ⢠Modifying external variables ā
ā ⢠Modifying function arguments (mutation) ā
ā ⢠console.log / DOM manipulation ā
ā ⢠HTTP requests (fetch, AJAX) ā
ā ⢠Reading from / writing to files ā
ā ⢠Accessing databases ā
ā ⢠Getting current date/time ā
ā ⢠Random number generation ā
ā ⢠Throwing exceptions ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Examples of Side Effects
// Modifying external variable
let count = 0;
function increment() {
count++; // Side effect
}
// Mutating argument
function addItem(arr, item) {
arr.push(item); // Side effect - mutates input
}
// Console output
function log(message) {
console.log(message); // Side effect
}
// API call
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`); // Side effect
return response.json();
}
// Current time
function getTimestamp() {
return Date.now(); // Side effect (non-deterministic)
}
Pure vs Impure Examples
Array Operations
// ā Impure - mutates original array
function addItemImpure(arr, item) {
arr.push(item);
return arr;
}
// ā
Pure - returns new array
function addItemPure(arr, item) {
return [...arr, item];
}
// ā Impure - mutates original
function removeFirstImpure(arr) {
arr.shift();
return arr;
}
// ā
Pure - returns new array
function removeFirstPure(arr) {
return arr.slice(1);
}
Object Operations
// ā Impure - mutates original object
function updateAgeImpure(person, newAge) {
person.age = newAge;
return person;
}
// ā
Pure - returns new object
function updateAgePure(person, newAge) {
return { ...person, age: newAge };
}
// ā Impure - mutates nested property
function updateAddressImpure(person, city) {
person.address.city = city;
return person;
}
// ā
Pure - returns new object with new nested object
function updateAddressPure(person, city) {
return {
...person,
address: { ...person.address, city },
};
}
String Operations
// ā
String methods are pure (strings are immutable)
function formatTitle(str) {
return str.toUpperCase().trim();
}
const original = ' hello world ';
const formatted = formatTitle(original);
console.log(original); // " hello world " (unchanged)
console.log(formatted); // "HELLO WORLD"
Math Operations
// ā
Pure mathematical functions
function square(x) {
return x * x;
}
function average(numbers) {
if (numbers.length === 0) return 0;
const sum = numbers.reduce((a, b) => a + b, 0);
return sum / numbers.length;
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
Benefits of Pure Functions
1. Predictability
// Always know what to expect
const result = multiply(4, 5); // Always 20
2. Testability
// Easy to test - no setup needed
function add(a, b) {
return a + b;
}
// Simple tests
console.assert(add(1, 2) === 3);
console.assert(add(-1, 1) === 0);
console.assert(add(0, 0) === 0);
3. Memoization
// Can safely cache results
function memoize(fn) {
const cache = {};
return function (...args) {
const key = JSON.stringify(args);
if (!(key in cache)) {
cache[key] = fn(...args);
}
return cache[key];
};
}
const expensiveCalculation = memoize((n) => {
console.log('Computing...');
return n * n;
});
expensiveCalculation(5); // "Computing..." ā 25
expensiveCalculation(5); // 25 (cached, no log)
4. Parallel Processing
// Safe to run in parallel - no shared state
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((n) => n * 2); // Each call is independent
5. Easier Debugging
// Can reason about each function in isolation
const data = getData();
const filtered = filterData(data);
const sorted = sortData(filtered);
const formatted = formatData(sorted);
// Each step is isolated and predictable
Common Side Effects
1. Mutating Arguments
// ā Mutates argument
function sortArray(arr) {
return arr.sort(); // sort() mutates!
}
// ā
Doesn't mutate
function sortArrayPure(arr) {
return [...arr].sort(); // Copy first
}
2. Using Global State
// ā Uses global state
let config = { debug: true };
function log(message) {
if (config.debug) {
console.log(message);
}
}
// ā
Pass config as argument
function logPure(message, debug) {
if (debug) {
console.log(message);
}
}
3. Date/Time
// ā Non-deterministic
function getAge(birthYear) {
return new Date().getFullYear() - birthYear;
}
// ā
Pass current year as argument
function getAgePure(birthYear, currentYear) {
return currentYear - birthYear;
}
4. Random Numbers
// ā Non-deterministic
function shuffle(arr) {
return arr.sort(() => Math.random() - 0.5);
}
// ā
Accept random function as parameter
function shufflePure(arr, randomFn) {
return [...arr].sort(() => randomFn() - 0.5);
}
// In tests, you can pass a predictable "random" function
Managing Side Effects
Isolate Side Effects
Push side effects to the edges of your application.
// Pure business logic
function calculateDiscount(price, discountPercent) {
return price * (1 - discountPercent / 100);
}
function formatPrice(price) {
return `$${price.toFixed(2)}`;
}
// Side effects at the boundary
async function processOrder(orderId) {
// Side effect: API call
const order = await fetchOrder(orderId);
// Pure functions for business logic
const discountedPrice = calculateDiscount(order.price, order.discount);
const formattedPrice = formatPrice(discountedPrice);
// Side effect: update database
await updateOrder(orderId, { finalPrice: discountedPrice });
// Side effect: log
console.log(`Order ${orderId}: ${formattedPrice}`);
}
Dependency Injection
// ā Hard-coded dependencies
function getUser(id) {
return fetch(`/api/users/${id}`).then((r) => r.json());
}
// ā
Dependencies injected
function createUserService(httpClient) {
return {
getUser(id) {
return httpClient.get(`/users/${id}`);
},
};
}
// Can inject mock for testing
const mockClient = { get: () => Promise.resolve({ id: 1, name: 'Test' }) };
const userService = createUserService(mockClient);
Command Query Separation
// ā
Queries (pure) - return data, no mutations
function getTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
function getActiveUsers(users) {
return users.filter((user) => user.active);
}
// ā
Commands (side effects) - perform actions
function saveOrder(order) {
database.save(order); // Side effect
}
function sendEmail(to, subject, body) {
emailService.send(to, subject, body); // Side effect
}
Immutability
Immutability is key to pure functions.
Array Immutability
const original = [1, 2, 3];
// ā
Immutable operations
const added = [...original, 4]; // [1, 2, 3, 4]
const removed = original.filter((x) => x !== 2); // [1, 3]
const updated = original.map((x) => x * 2); // [2, 4, 6]
const sorted = [...original].sort(); // [1, 2, 3]
// ā Mutable operations (avoid)
original.push(4); // Mutates!
original.sort(); // Mutates!
original.splice(1, 1); // Mutates!
Object Immutability
const person = { name: 'Alice', age: 25 };
// ā
Immutable updates
const older = { ...person, age: 26 };
const renamed = { ...person, name: 'Alicia' };
// Nested updates
const withAddress = {
...person,
address: { city: 'NYC', zip: '10001' },
};
// Updating nested property
const newCity = {
...withAddress,
address: { ...withAddress.address, city: 'LA' },
};
Deep Immutability
// Deep update helper
function updateIn(obj, path, value) {
const [first, ...rest] = path;
if (rest.length === 0) {
return { ...obj, [first]: value };
}
return {
...obj,
[first]: updateIn(obj[first] || {}, rest, value),
};
}
const state = {
user: {
profile: {
name: 'Alice',
},
},
};
const newState = updateIn(state, ['user', 'profile', 'name'], 'Bob');
Referential Transparency
An expression is referentially transparent if it can be replaced with its value without changing the program's behavior.
// Referentially transparent
const x = add(2, 3); // Can replace with 5
const y = x + x; // Can replace with 5 + 5
// Not referentially transparent
const now = Date.now(); // Can't replace - value changes
const random = Math.random(); // Can't replace - value changes
Benefits
// Can reason about code mathematically
const result = add(multiply(2, 3), multiply(2, 3));
// Can simplify to:
const temp = multiply(2, 3); // 6
const result = add(temp, temp); // 12
// Or directly:
const result = add(6, 6); // 12
Best Practices
1. Prefer Pure Functions
// ā
Pure by default
function processData(data) {
return data
.filter((item) => item.active)
.map((item) => transform(item))
.sort((a, b) => a.name.localeCompare(b.name));
}
2. Don't Mutate Arguments
// ā Mutates input
function addDefaults(config) {
config.timeout = config.timeout || 5000;
return config;
}
// ā
Returns new object
function addDefaults(config) {
return {
timeout: 5000,
...config,
};
}
3. Return New Data Structures
// ā
Always return new arrays/objects
function addUser(users, newUser) {
return [...users, newUser];
}
function updateUser(users, id, updates) {
return users.map((user) => (user.id === id ? { ...user, ...updates } : user));
}
function removeUser(users, id) {
return users.filter((user) => user.id !== id);
}
4. Isolate Side Effects
// ā
Side effects clearly separated
class OrderProcessor {
// Pure methods
calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
applyDiscount(total, discount) {
return total * (1 - discount);
}
// Side effects (clearly labeled)
async saveOrder(order) {
await database.save(order);
}
async sendConfirmation(email, order) {
await emailService.send(email, order);
}
}
5. Use Immutable Data Patterns
// ā
Consistent immutable patterns
const addTodo = (todos, text) => [
...todos,
{ id: Date.now(), text, completed: false },
];
const toggleTodo = (todos, id) =>
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
const removeTodo = (todos, id) => todos.filter((todo) => todo.id !== id);
Summary
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā PURE FUNCTIONS & SIDE EFFECTS ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā PURE FUNCTIONS: ā
ā ⢠Same input ā same output ā
ā ⢠No side effects ā
ā ⢠Depend only on arguments ā
ā ā
ā SIDE EFFECTS: ā
ā ⢠Mutating external state ā
ā ⢠I/O operations (console, files, network) ā
ā ⢠Date/time, random numbers ā
ā ⢠DOM manipulation ā
ā ā
ā BENEFITS OF PURE: ā
ā ⢠Predictable ā
ā ⢠Testable ā
ā ⢠Cacheable (memoization) ā
ā ⢠Parallelizable ā
ā ⢠Easy to debug ā
ā ā
ā BEST PRACTICES: ā
ā ⢠Prefer pure functions ā
ā ⢠Don't mutate arguments ā
ā ⢠Return new data structures ā
ā ⢠Isolate side effects ā
ā ⢠Use dependency injection ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Next Steps
- ā¢Practice with the examples in
examples.js - ā¢Complete the exercises in
exercises.js - ā¢Learn about functional programming patterns
- ā¢Explore immutable data libraries (Immer, Immutable.js)