Skip to main content

Ensure Immutability : The Decorator Approach

· 3 min read
Samuel Joset

Introduction

Who hasn't encountered those insidious bugs caused by variables or objects mysteriously changing their values during code execution? I won't even ask if this has happened to you, as the answer is easy to guess.

I would not say that variable mutability is a bad thing. In fact, it can be very useful to save memory when operating in an environment with limited RAM, such as on a Raspberry Pi or an embedded device.

However, it can also lead to undesirable side effects and make our code harder to understand and debug. That's why immutability is a fundamental principle in many programming languages and paradigms, including functional programming!

In a previous article, we explored the many advantages offered by immutability. We also learned how to extend the functionalities of our functions using decorators.

Now, we're going to combine these two concepts and see how to make an initially mutable program, immutable with the help of a decorator. Are you ready? Let's go!

2. The Immutability Decorator

This article gets straight to the point. We will create a decorator that turns mutable functions into immutable ones, explain how it works, its usefulness, and its limitations.

To remind you, a decorator is a function that takes another function as an argument and returns a new function that extends the functionality of the original one. In our case, our decorator will deeply copy all the parameters of the original function (thus ensuring their immutability) before executing it. For simplicity, we will use Ramda's clone function to make the copies:

const R = require('ramda');

const makeImmutable = (fn) => {
return (...args) => {
const deepCopiedArgs = args.map(arg => R.clone(arg));
return fn(...deepCopiedArgs);
};
};

In this way, even if fn modifies its arguments, it will not affect the original objects passed in as parameters. This is a way to get immutable versions of our functions while keeping the original functions if needed. The code is elegant and reusable.

3. Application

Now, let's see how to apply our new decorator to an existing function. To illustrate this, we will focus on the classic case of bubble sort. This is a well-known algorithm that, in its simplest version, modifies the array it is sorting. However, thanks to our decorator, we are going to teach it good manners and ensure that it preserves the immutability of the parameters it receives.

Let's start by creating our bubble sort function:

const bubbleSortMutable = (array) => {
let n = array.length;
for(let i = 0; i < n-1; i++) {
for(let j = 0; j < n-i-1; j++) {
if(array[j] > array[j+1]) {
// Swap array[j] and array[j+1]
let temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
return array;
};

This function will correctly sort our array, but by modifying it. To solve this problem, we will apply our makeImmutable decorator to our bubble sort function:

const bubbleSortImmutable = makeImmutable(bubbleSortMutable);

Our bubbleSortImmutable function will do exactly the same task as bubbleSortMutable, BUT without modifying the original array.

let numbers = [5, 2, 9, 1, 5, 6];
console.log(bubbleSortImmutable(numbers)); // displays [1, 2, 5, 5, 6, 9]
console.log(numbers); // displays [5, 2, 9, 1, 5, 6]

Mission accomplished! Even after sorting our array with bubbleSortImmutable, our original array remains unchanged.

4. Advanced Uses and Limits of makeImmutable

Now that we have covered the basics of our makeImmutable decorator, it's time to take a look at some of its more advanced uses and limitations.

4.1. Advanced Uses

Beyond protecting against undesirable mutability, our makeImmutable decorator can be used to create "snapshots" of an object's state before and after a function's execution. This can prove invaluable in testing environments, when we want to verify whether a function alters an object unexpectedly.

4.2. Limits of makeImmutable

While the makeImmutable decorator is a powerful tool for managing mutability, it has its limitations. For example, it cannot prevent a function from modifying global objects or object properties that are accessible through other means (such as through the use of this in JavaScript).

5. Conclusion

We have come to the end of our article. Rest assured, no arrays were harmed during this demonstration. The function we created is very useful for applying the concept of immutability in a simple way within our program. However, it can never replace a well-thought-out and organized code architecture. Therefore, it is essential to know when and where to use it properly.