Sunday, March 2, 2025
Higher-order functions, immutability, and pure functions
Posted by

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.