Skip to main content

JavaScript Pure Functions

Introduction

Pure functions are one of the fundamental building blocks of functional programming in JavaScript. A pure function is a function that, given the same input, will always return the same output and has no side effects. This simple concept has profound implications for writing clean, testable, and predictable code.

In this tutorial, we'll explore what pure functions are, why they're important, and how to use them effectively in your JavaScript applications.

What Is a Pure Function?

A pure function must satisfy two criteria:

  1. Deterministic: Given the same input, it always returns the same output
  2. No Side Effects: It doesn't modify anything outside of its scope (no external state changes)

Let's look at an example of a pure function:

javascript
function add(a, b) {
return a + b;
}

console.log(add(2, 3)); // Output: 5
console.log(add(2, 3)); // Output: 5 (always the same result for same inputs)

No matter how many times you call add(2, 3), it will always return 5. The function doesn't rely on any external data and doesn't modify any state outside its scope.

Identifying Impure Functions

To better understand pure functions, let's examine some examples of impure functions:

Example 1: Using External Variables

javascript
let counter = 0;

function incrementCounter(value) {
counter += value;
return counter;
}

console.log(incrementCounter(2)); // Output: 2
console.log(incrementCounter(2)); // Output: 4 (different result for same input)

This function is impure because:

  • It modifies an external variable (counter)
  • Given the same input (2), it returns different outputs

Example 2: Performing I/O Operations

javascript
function logMessage(message) {
console.log(message); // Side effect: writing to console
return message;
}

This function is impure because it has a side effect (writing to the console).

Example 3: Modifying Input Parameters

javascript
function addToArray(arr, item) {
arr.push(item); // Modifies the original array
return arr;
}

const numbers = [1, 2, 3];
console.log(addToArray(numbers, 4)); // Output: [1, 2, 3, 4]
console.log(numbers); // Output: [1, 2, 3, 4] (original array modified)

This function is impure because it modifies its input parameter (the array).

Converting Impure Functions to Pure Functions

Let's rewrite the impure functions above to make them pure:

Example 1: Using Return Values Instead of External Variables

javascript
function pureIncrement(counter, value) {
return counter + value;
}

let counter = 0;
counter = pureIncrement(counter, 2);
console.log(counter); // Output: 2

counter = pureIncrement(counter, 2);
console.log(counter); // Output: 4

Example 2: Avoiding I/O in the Function

javascript
function createMessage(message) {
return `Message: ${message}`;
}

// The I/O happens outside the function
const msg = createMessage("Hello");
console.log(msg);

Example 3: Creating New Data Structures Instead of Modifying Existing Ones

javascript
function pureAddToArray(arr, item) {
return [...arr, item]; // Creates a new array
}

const numbers = [1, 2, 3];
const newNumbers = pureAddToArray(numbers, 4);

console.log(newNumbers); // Output: [1, 2, 3, 4]
console.log(numbers); // Output: [1, 2, 3] (original array unchanged)

Benefits of Pure Functions

1. Predictability

Pure functions make your code more predictable because they always produce the same output for the same inputs, regardless of when or where they are called.

2. Testability

Since pure functions don't depend on external state, they are much easier to test. You don't need complex mocks or test setups.

javascript
// Easy to test pure function
function multiply(a, b) {
return a * b;
}

// Test
console.log(multiply(2, 3) === 6); // Output: true
console.log(multiply(0, 5) === 0); // Output: true
console.log(multiply(-1, 8) === -8); // Output: true

3. Concurrency

Pure functions can be run in parallel without conflicts since they don't share or modify external state.

4. Memoization

Since pure functions always return the same output for the same input, we can cache (memoize) results to improve performance:

javascript
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
console.log('Returning from cache');
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}

// A potentially expensive pure function
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}

const memoizedFib = memoize(fibonacci);

console.time('First call');
console.log(memoizedFib(30)); // Computes the result
console.timeEnd('First call');

console.time('Second call');
console.log(memoizedFib(30)); // Returns from cache
console.timeEnd('Second call');

5. Easier Debugging

When functions are pure, bugs are easier to locate because the function's behavior is isolated from the rest of the code.

Real-world Applications

Data Transformation in Frontend Applications

Pure functions are excellent for data transformations in React or Vue applications:

javascript
// Transform API data for display
function formatUserData(user) {
return {
fullName: `${user.firstName} ${user.lastName}`,
displayEmail: user.email.toLowerCase(),
memberSince: new Date(user.joinDate).toLocaleDateString()
};
}

// API data
const apiUser = {
firstName: 'John',
lastName: 'Doe',
email: '[email protected]',
joinDate: '2023-01-15T00:00:00Z'
};

// Transformed data for UI
const displayUser = formatUserData(apiUser);
console.log(displayUser);
// Output:
// {
// fullName: 'John Doe',
// displayEmail: '[email protected]',
// memberSince: '1/15/2023' (format depends on locale)
// }

Functional Utility Libraries

Pure functions form the foundation of functional programming libraries like Lodash, Ramda, and Underscore:

javascript
// Our own mini utility library with pure functions
const utils = {
sum: array => array.reduce((acc, val) => acc + val, 0),
average: array => array.length ? utils.sum(array) / array.length : 0,
max: array => Math.max(...array),
min: array => Math.min(...array)
};

const temperatures = [72, 68, 74, 77, 71];

console.log(`Sum: ${utils.sum(temperatures)}`); // Output: Sum: 362
console.log(`Average: ${utils.average(temperatures)}`); // Output: Average: 72.4
console.log(`Max: ${utils.max(temperatures)}`); // Output: Max: 77
console.log(`Min: ${utils.min(temperatures)}`); // Output: Min: 68

State Management

Pure functions are crucial for managing state in applications using Redux:

javascript
// A reducer (pure function) for a todo application
function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, {
id: Date.now(),
text: action.text,
completed: false
}];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
);
default:
return state;
}
}

// Initial state
let todos = [];

// Add a todo (simulating Redux dispatch)
todos = todosReducer(todos, { type: 'ADD_TODO', text: 'Learn pure functions' });
console.log(todos);
// Output: [{ id: (timestamp), text: 'Learn pure functions', completed: false }]

// Toggle a todo
todos = todosReducer(todos, { type: 'TOGGLE_TODO', id: todos[0].id });
console.log(todos);
// Output: [{ id: (timestamp), text: 'Learn pure functions', completed: true }]

Common Challenges with Pure Functions

Working with JavaScript's Impure Standard Library

Many built-in JavaScript methods are impure. For example, Array.prototype.sort() modifies the original array. To write pure code, you need to create copies:

javascript
// Impure way
const numbers = [3, 1, 2];
numbers.sort();
console.log(numbers); // Output: [1, 2, 3] (original array modified)

// Pure way
const numbers2 = [3, 1, 2];
const sortedNumbers = [...numbers2].sort();
console.log(numbers2); // Output: [3, 1, 2] (original unchanged)
console.log(sortedNumbers); // Output: [1, 2, 3]

Dealing with Side Effects

In real applications, side effects are unavoidable (HTTP requests, database operations, etc.). The functional approach is not to eliminate side effects entirely, but to isolate and manage them:

javascript
// Separate pure business logic from side effects
// Pure function for calculating total
function calculateOrderTotal(items) {
return items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}

// Function with side effects
async function processOrder(items) {
// Calculate total using pure function
const total = calculateOrderTotal(items);

// Side effect: API call
try {
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items, total })
});
return await response.json();
} catch (error) {
console.error('Error processing order:', error);
throw error;
}
}

Summary

Pure functions are a fundamental concept in functional programming that leads to more predictable, testable, and maintainable code. A pure function always returns the same output for the same inputs and has no side effects.

Key takeaways:

  • Pure functions are deterministic and have no side effects
  • They make code more predictable and easier to test
  • They enable optimizations like memoization
  • Real-world applications include data transformation, state management, and building utility libraries
  • While side effects can't be eliminated completely, they can be isolated and managed effectively

Exercises

  1. Identify whether the following functions are pure or impure, and explain why:

    javascript
    function double(x) { return x * 2; }
    function getRandomNumber() { return Math.random(); }
    function addToCart(cart, item) { cart.push(item); return cart; }
  2. Convert the following impure function to a pure function:

    javascript
    function updateUser(user, prop, value) {
    user[prop] = value;
    return user;
    }
  3. Create a pure function that takes an array of objects and returns a new array with only the objects that match certain criteria.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)