Introduction to Functional Programming in JavaScript: Immutability #6

RMAG news

Immutability is a key concept in functional programming and is crucial for writing reliable, maintainable, and predictable code. By ensuring that data objects do not change after they are created, immutability helps to eliminate side effects and makes it easier to reason about the state of your application.

What is Immutability?

Immutability means that once an object is created, it cannot be changed. Instead of modifying an object, you create a new object with the desired changes. This contrasts with mutable objects, which can be modified after they are created.

Immutability can be applied to various types of data, including numbers, strings, arrays, and objects. Primitive values (numbers, strings, booleans) are inherently immutable in JavaScript, but complex data structures like arrays and objects are mutable by default.

Why is Immutability Important?

Predictability: Immutable data ensures that objects do not change unexpectedly, making the behavior of your program more predictable and easier to understand.

Debugging: When data is immutable, you can be confident that once it is created, it remains unchanged, which simplifies debugging and tracing the flow of data through your application.

Concurrency: Immutability helps avoid issues related to concurrent modifications of shared data, which is particularly important in multi-threaded or asynchronous environments.

Time-Travel Debugging: With immutable data, you can easily implement features like time-travel debugging, where you can step back and forth through the history of state changes in your application.

Achieving Immutability in JavaScript

While JavaScript does not enforce immutability by default, there are several techniques and libraries you can use to achieve immutability in your code.

Using const for Primitive Values

const x = 42;
// x = 43; // This will cause an error because x is immutable

Declaring a variable with const ensures that the variable cannot be reassigned, making it immutable.

Immutable Arrays

To achieve immutability with arrays, you can use methods that do not mutate the original array, such as map, filter, concat, and the spread operator.

const arr = [1, 2, 3];

// Using map
const doubled = arr.map(x => x * 2);

// Using filter
const evens = arr.filter(x => x % 2 === 0);

// Using concat
const extended = arr.concat([4, 5]);

// Using spread operator
const withNewElement = […arr, 4];

console.log(arr); // [1, 2, 3]
console.log(doubled); // [2, 4, 6]
console.log(evens); // [2]
console.log(extended); // [1, 2, 3, 4, 5]
console.log(withNewElement); // [1, 2, 3, 4]

Immutable Objects

For objects, you can use Object.assign and the spread operator to create new objects with updated properties.

const obj = { a: 1, b: 2 };

// Using Object.assign
const updatedObj = Object.assign({}, obj, { b: 3 });

// Using spread operator
const updatedObj2 = { obj, b: 3 };

console.log(obj); // { a: 1, b: 2 }
console.log(updatedObj); // { a: 1, b: 3 }
console.log(updatedObj2); // { a: 1, b: 3 }

Deep Immutability

For deeply nested structures, achieving immutability can be more challenging. Libraries like Immutable.js and Immer provide tools for creating and managing immutable data structures.

const { Map } = require(immutable);

const obj = Map({ a: 1, b: 2 });
const updatedObj = obj.set(b, 3);

console.log(obj.toObject()); // { a: 1, b: 2 }
console.log(updatedObj.toObject()); // { a: 1, b: 3 }

“`javascript
const produce = require(‘immer’).produce;

const obj = { a: 1, b: 2 };

const updatedObj = produce(obj, draft => {
draft.b = 3;
});

console.log(obj); // { a: 1, b: 2 }
console.log(updatedObj); // { a: 1, b: 3 }
“`

Object.freeze

You can use Object.freeze to make an object immutable. However, this is a shallow freeze, meaning nested objects can still be modified.

const obj = Object.freeze({ a: 1, b: { c: 2 } });

// obj.a = 3; // This will cause an error
obj.b.c = 3; // This will not cause an error because the freeze is shallow

console.log(obj); // { a: 1, b: { c: 3 } }

To achieve deep immutability, you can use recursive freezing:

function deepFreeze(obj) {
Object.keys(obj).forEach(prop => {
if (typeof obj[prop] === object && obj[prop] !== null) {
deepFreeze(obj[prop]);
}
});
return Object.freeze(obj);
}

const obj = deepFreeze({ a: 1, b: { c: 2 } });

// obj.a = 3; // This will cause an error
// obj.b.c = 3; // This will also cause an error

console.log(obj); // { a: 1, b: { c: 2 } }