Back to blog

Sunday, March 2, 2025

Higher-order functions, immutability, and pure functions

cover

Functional programming is a paradigm that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. While JavaScript is not a purely functional programming language, it supports many functional programming concepts. In this blog, we will explore some of the core concepts of functional programming in JavaScript, including higher-order functions, immutability, and pure functions. These concepts not only make your code more predictable and easier to test but also help you write more efficient applications.

Higher-order Functions

A higher-order function is a function that either takes one or more functions as arguments or returns one or more functions. This feature is fundamental to JavaScript and enables us to write more abstract and reusable code.

Functions as First-class Citizens

In JavaScript, functions are first-class citizens, meaning you can:

  • Assign functions to variables
  • Pass functions as arguments to other functions
  • Return functions from other functions
Assigning Functions to Variables

Here’s a simple example of assigning a function to a variable:

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

const sum = add;
console.log(sum(5, 3)); // Outputs: 8
Passing Functions as Arguments

This is commonly used in array methods like map, filter, and reduce:

const numbers = [1, 2, 3, 4, 5];

const doubledNumbers = numbers.map(function(number) {
    return number * 2;
});

console.log(doubledNumbers); // Outputs: [2, 4, 6, 8, 10]
Returning Functions from Other Functions

Functions that take other functions as arguments or return functions are known as closures:

function multiplier(factor) {
    return function(number) {
        return number * factor;
    };
}

const double = multiplier(2);
console.log(double(5)); // Outputs: 10

Pure Functions

A pure function is a function where the return value is determined solely by its input values, without observable side effects. Pure functions have the following characteristics:

  • Deterministic: Given the same input, a pure function will always return the same output.
  • No Side Effects: Pure functions do not cause any observable side effects such as modifying global variables, altering input parameters, or logging to the console.
Benefits of Pure Functions
  • Predictable: The output of a pure function is always predictable, which makes debugging easier.
  • Testable: Functions that do not rely on outside variables or states are easier to test.
  • Cacheable: The results of a pure function can be cached based on its inputs.
Example of a Pure Function

Here’s a simple example of a pure function:

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

console.log(add(2, 3)); // Outputs: 5
Example of an Impure Function

In contrast, here’s an example of an impure function, which relies on a global variable:

let counter = 0;

function increment() {
    return ++counter;
}

console.log(increment()); // Outputs: 1
console.log(increment()); // Outputs: 2

In this example, the increment function is impure because it modifies a global variable counter. Each time the function is called, the output varies even though the input parameters remain the same.

Immutability

Immutability refers to the concept of keeping data structures constant and unchangeable once they are created. Instead of modifying existing data, immutable functions create new derived data. This approach has several advantages, including:

  • Predictability: Immutable data structures make it easier to reason about the behavior of your application.
  • Concurrent Programming: Immutable data can be shared between threads or processes without risking inconsistencies.
  • Memoization: Since the data doesn’t change, it can be cached effectively.

Creating Immutable Data Structures

JavaScript provides several ways to create and work with immutable data structures. Let's explore a few common techniques:

Using Spread Operator

The spread operator (...) can be used to create a shallow copy of arrays and objects:

const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];

console.log(arr1); // Outputs: [1, 2, 3]
console.log(arr2); // Outputs: [1, 2, 3, 4, 5]

const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 };

console.log(obj1); // Outputs: { a: 1, b: 2 }
console.log(obj2); // Outputs: { a: 1, b: 2, c: 3 }
Using Object.freeze

The Object.freeze method can be used to make an object immutable:

const person = {
    name: "John",
    age: 30
};

Object.freeze(person);

person.name = "Jane"; // No effect in strict mode
console.log(person.name); // Outputs: John

The Object.freeze method only freezes the object at the first level. If the object contains nested objects or arrays, those can still be modified. For deep freezing, you would need to write a custom function.

Using Libraries

For more complex and deep immutability, consider using libraries like Immutable.js or Immer. These libraries provide powerful tools to handle immutable data in larger applications.

// Example using Immutable.js
import { Map } from 'immutable';

const map1 = Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 50);

console.log(map1.get('b')); // Outputs: 2
console.log(map2.get('b')); // Outputs: 50

Benefits of Functional Programming

Functional programming in JavaScript has many benefits, including:

  • Maintainable Code: Pure functions and immutable data make code easier to understand and maintain.
  • Scalable Applications: Immutable data and higher-order functions make it easier to scale applications.
  • Modular Code: Functional programming promotes modular code that can be easily tested and reused.
  • Simplified Debugging: Since pure functions have no side effects, debugging becomes more straightforward.

Modifying Data Functionally

Instead of mutating existing data, functional programming encourages creating new derived data. Here’s how you can achieve this in JavaScript.

Mapping Arrays

The map function is a common higher-order function used to create a new array with the results of calling a provided function on every element in the calling array.

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(number => number * 2);

console.log(doubled); // Outputs: [2, 4, 6, 8, 10]
Filtering Arrays

The filter function creates a new array with all elements that pass the test implemented by the provided function.

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(number => number % 2 === 0);

console.log(evenNumbers); // Outputs: [2, 4]
Reducing Arrays

The reduce function executes a reducer function on each element of the array, resulting in a single output value.

const numbers = [1, 2, 3, 4, 5];
const total = numbers.reduce((acc, number) => acc + number, 0);

console.log(total); // Outputs: 15
Updating Nested Objects

For nested objects, you can use the spread operator and object destructuring to create a new object without modifying the original one.

const person = {
    name: "John",
    address: {
        city: "New York",
        zip: "10001"
    }
};

const updatedPerson = {
    ...person,
    address: {
        ...person.address,
        city: "Los Angeles"
    }
};

console.log(updatedPerson); // Outputs: { name: 'John', address: { city: 'Los Angeles', zip: '10001' } }
console.log(person); // Outputs: { name: 'John', address: { city: 'New York', zip: '10001' } }

Common Techniques in Functional Programming

Functional programming promotes a set of techniques that make code more predictable and easier to manage:

Composition

Function composition is the process of combining two or more functions to produce a new function. This technique encourages modularity and code reusability.

function add(x) {
    return function(y) {
        return x + y;
    };
}

const add5 = add(5);
console.log(add5(10)); // Outputs: 15

Currying

Currying is a technique where a function of multiple arguments is transformed into a sequence of unary functions (functions that take a single argument).

function sum(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        };
    };
}

const sumABC = sum(1)(2)(3);
console.log(sumABC); // Outputs: 6

Recursion

Recursion is another common technique in functional programming, where a function calls itself to solve smaller instances of a problem.

function factorial(n) {
    if (n === 0) {
        return 1;
    }
    return n * factorial(n - 1);
}

console.log(factorial(5)); // Outputs: 120

Real-world Usage of Functional Programming in JavaScript

Functional programming techniques are widely used in real-world JavaScript applications, especially in frameworks like React and Redux.

Redux

Redux, a popular state management library for JavaScript applications, heavily relies on functional programming principles. Here’s an example of a pure function used as a reducer in Redux:

const initialState = {
    todos: []
};

function todoReducer(state = initialState, action) {
    switch (action.type) {
        case 'ADD_TODO':
            return {
                ...state,
                todos: [...state.todos, action.payload]
            };
        default:
            return state;
    }
}

In this example, the todoReducer is a pure function that returns a new state object instead of modifying the existing one.

React

React, a popular JavaScript library for building user interfaces, also benefits from functional programming principles. Functional components in React are simple JavaScript functions that return JSX.

function Greeting(props) {
    return <h1>Hello, {props.name}!</h1>;
}

// Usage
<Greeting name="Alice" />

Functional components can utilize hooks to manage state and side effects, which are also designed to remain functional and predictable.

Conclusion

Functional programming is a powerful paradigm that can make your JavaScript code more predictable and easier to test. By leveraging higher-order functions, pure functions, and immutability, you can write cleaner and more maintainable applications. As you continue to explore these concepts, you'll see how they can be applied to various real-world scenarios, making your codebase more robust and scalable.

Feel free to experiment with these concepts in your projects to see the benefits of functional programming in action.