dna

Beyond State

Exploring Mutability, Immutability, and Copying in Programming

Immutability is a fundamental pillar within the functional programming paradigm. This allows us to write robust, simple, and reliable code. An immutable object, once created, maintains an unchanging state; no function can change it. Instead, functions operate by taking an input and returning a new object as output, leaving the original object untouched.

Before we explore the risks of working with mutable data, it is important to go through some fundamental concepts.

Concepts of Mutability and Immutability

In programming language, mutability and immutability are concepts related to the possibility of the state of an object being changed after its creation. In mutable data, you can modify the state, while in immutable data, the state cannot be changed after creation.

Immutability

In many programming languages, primitives are typically immutable. Primitives are basic data types and values provided by the language, and their values cannot be changed after they are created.

In JavaScript, for example, we have 7 primitives:

  • string;
  • number;
  • bigint;
  • boolean;
  • undefined;
  • symbol;
  • null;

All those primitives are immutable.

// trying to mutate a string

let name = 'Tarah'

console.log(name[0]) // T

name[0] = 'S'

console.log(name) // Tarah

In this example, we have a string name initialized with the value ‘Tarah’. When we attempt to access the first character using name[0], we correctly get ‘T’, which is the value located at index 0 of the string.

When we attempt to change the first character to ‘S’ using name[0] = 'S', it won’t mutate the string. Strings in JavaScript are immutable, meaning their values cannot be changed once they are created. So, the attempt to change the character at index 0 has no effect. The final console.log(name) still outputs ‘Tarah’, not ‘Sarah’.

However, according to MDN Web Docs, it is important not to confuse a primitive itself with a variable assigned a primitive value. The variable may be reassigned to a new value, but the existing value cannot be changed.

// reassign the variable to a new value

let name = 'Tarah'

name = 'Sarah'

console.log(name) // Sarah

In this example, we declare a variable name with the value ‘Tarah’. Later, we reassign the variable to a new value with name = 'Sarah'. Unlike the first example, this does work because we’re not trying to modify the existing string; instead, we’re creating a new string and assigning it to the variable name. The final console.log(name) outputs ‘Sarah’ as expected.

Mutability

On the other hand, non-primitive data is mutable in most programming languages. In JavaScript, objects (includes array, function, date and regular expression) are considered non-primitive.

let names = ['Mine', 'Noah', 'Mary']

names[1] = 'John'

console.log(names) // ['Mine', 'John', 'Mary']

In this JavaScript snippet, we have an array called ‘names’ initially containing ‘Mine’, ‘Noah’, and ‘Marry’. When we use names[1] = 'John', we’re directly updating the second element of the array (index 1) from ‘Noah’ to ‘John’. Arrays in JavaScript are mutable, allowing us to directly modify their elements. The final output reveals the modified array as ['Mine', 'John', 'Marry'].

It is important to highlight that, even declaring the variable names as a constant (const names = ['Mine', 'Noah', 'Mary']), this does not change the mutable nature of the object. In this case, the variable cannot be reassigned to a new value, while the object itself can mutate.

Concepts of Copy

How an object is copied can significantly impact the mutability of the original object. Let’s explore how different copying techniques affect the mutability of the main object.

To illustrate the concepts of copying in programming, we will use an object as a base:

const person = {
    name: 'Mine',
    age: 51,
    favoriteMovies: ['Mad Max', 'Batman'],
}

This object, named person, contains information about an individual, including their name, age, and a list of favorite movies. Throughout the explanation, we will use this object to demonstrate the concepts of copying: Reference Copy, Shallow Copy, and Deep Copy.

Reference Copy

In a Reference Copy, both the original and the copied variables point to the same object in memory. Consequently, changes made to one variable directly affect the other.

reference copy in memory

const person2 = person

person2.age = 63

console.log(person.age) // 63

console.log(person2.age) // 63

Modifying the age property of person2 also changes the age property of the original person because they both refer to the same object in memory, illustrating the concept of mutation.

Shallow copy

A shallow copy (shallow immutability) creates a new object by copying the top-level properties of the original object, not the values of the nested objects. Nested values are still references to the original object’s properties.

shallow copy in memory

const person2 = {...person}

person2.age = 63

console.log(person.age) // 51

console.log(person2.age) // 63

In this example, person2 is a shallow copy of person. The age property of person2 is modified independently, and it does not affect the original object in memory.

However, it is crucial to note that the shallow copy retains references to existing nested objects contained in the original object. Consequently, once these are modified within the copied object, there will be impact on the original one.

const person2 = {...person}

person2.favoriteMovies[0] = 'The Godfather'

console.log(person.favoriteMovies) // ['The Godfather', 'Batman']

console.log(person2.favoriteMovies) // ['The Godfather', 'Batman']

In this case, although person2 is a shallow copy of person, changes made to the favoriteMovies array within person2 also affect the original object in memory.

Deep copy

A Deep Copy (deep immutability), on the other hand, creates an independent copy of the original object, including all nested objects, mitigating mutability concerns:

deep copy in memory

As we are using JavaScript in our examples, it is important to note that standard built-in object-copy operations (such as the spread operator, Object.assign(), Object.create(), etc.) generate shallow copies of the original object. If a deep copy is needed for JavaScript objects and serialization is applicable, one approach is to employ JSON.stringify() to convert the object into a JSON string, followed by JSON.parse() to transform the string back into an entirely new JavaScript object:

const person2 = JSON.parse(JSON.stringify(person));

person2.favoriteMovies[0] = 'The Godfather'

console.log(person) // ['Mad Max', 'Batman']

console.log(person2) // ['The Godfather', 'Batman']

In this example, person2 is a deep copy of person. Modifying the favoriteMovies array within object2 does not impact the mutability of the original object in memory. This technique ensures the creation of an independent copy with no shared references to nested objects, providing a true deep copy of the original structure.

Mutability risks

So far, we’ve covered concepts such as mutability, immutability, and different types of copying. However, what are the real problems caused by mutability that a developer may face in their day-to-day work, leading to headaches?

Unexpected side effects:

We can define a side effect as a state modification. A side effect occurs when a function or expression changes (mutates) an object outside its local scope.

When objects are mutable, changes made by one part of the code can impact other parts unintentionally. This makes it more challenging to predict and control program behavior and difficult to track bugs, which brings us to the next topic.

Difficulty in Tracking Changes and debugging:

Debugging becomes more complex with mutable states. Identifying the source of a bug or unexpected behavior is challenging when the state of an object can change dynamically during program execution. This complexity can increase the time and effort required to locate and fix bugs.

Code Maintenance Challenges:

Immutable types are easier to understand and more reliable, once you can be sure that their state will not change. When an object or its reference is always mutable, it becomes more difficult to understand the code. Additionally, as the codebase evolves and new features are added, side effects can increase the risk of introducing bugs.

Conclusions

In this article, we explore the concepts of mutability and immutability, along with deep copy and shallow copy techniques. Furthermore, we illustrate how each of these copying methods handles mutability. Finally, we discuss some risks that mutable code can present.

Immutability provides a robust foundation for code reliability and ease of understanding. The risks associated with mutability, such as unexpected side effects and debugging complexities, highlight the importance of writing immutable code whenever possible, employing deep copy techniques, and carefully managing mutable states.

References

We want to work with you. Check out our "What We Do" section!