Immutability from experience: part 1

This is a 3 part series about what I have learnt from applying the immutable design pattern to my work in the recent months. This is born out of my frustration/confusion on this learning journey and hope that I can save someone from going through my confusion.

Image for post
Image for post

Part 1: Why immutable?

So what is immutability (immutable)? In layman’s terms, it’s something that cannot be change. In technical terms from Oxford, “unchanging over time or unable to be changed.

Image for post
Image for post

As a rather recent Software Engineer graduate, I was genuinely confused when I first faced this concept 😣.

  • Why would we want anything in computing to be immutable?
  • Isn’t the purpose of computing to compute?
  • Isn’t the ability to change (mutate) a value crucial for any software?

In Part 1 of this series, I would attempt to answer these questions and hopefully shed some light about how immutability is a design pattern that we should try to apply it by default.

Let’s look at the following code snippet¹.

Assuming that modules A and B are called by different parts of the system at the same time. Could you tell me, what is the value of x in line 4 and 8?

This is a classic race condition that we are taught in computing 101. Depending on which module gets to run first, value of x in line 4 could be either 2 or 4, and x in line 8 could be 3 or 4. Imagine if this was an object with more properties inside, how could you ever tell what’s the value of a variable in any point in time without stepping through every single execution call?

Well, what if we make all variables a constant? How would it look like?

In this example, what’s x at line 6 and 11?

But look! We suddenly know all the values from the resulting operations in Module A and B, and all we did were to change the variables from mutable let, to a constant const.

This brings me to the first and the most important benefit of immutability.

(1) Thread safety

By making your variables immutable, you avoid all critical race conditions, which has very real benefits in the real world.

No run-time bug that can’t be reproduced

How many times have you asked the question, “can you reproduce it?”. Well this isn’t a magic bullet that can help you reproduce any bug, but chances are, if you were to run your application in debug mode, the values for whatever you’re looking at would not be magically changed by other badly behaving code.

Image for post
Image for post

Allows concurrent processing

Because your code is thread-safe, now you can run things concurrently². For example, if you want to count the number of lines in 3 files, you can run the count() function on 3 separate threads because there is no critical section.

In one of the file handling services I was developing, this allowed me to split up two workflows into their own threads so that I can handle bi-directional flow at the same time (incoming and outgoing files). Because I know that it is thread safe, it literally took me 10 mins to refactor a single-threaded, synchronous application into a multi-threaded, asynchronous application.

While I can think of many reasons³ why this pattern isn’t adopted by default, I think the main reason is because the first thing computing 101 teaches us is mathematically wrong.

// Computer
x = 1
x = x + 1
// Math
x = 1
y = x + 1

We would never write x = x + 1 in math, why do we write that in our programming languages?! Let that sink in for a moment…

(2) Consistent states

This brings me to the next benefit. Because the variables are immutable…

There are no invalid intermediate states.

What’s an intermediate state? It’s the state of an object during the Transform function shown in the diagram below.

Image for post
Image for post

Let’s take a look pseudo code example for the Transform() function. When we try to transform the object from an Apple 🍎 to an Orange 🍊, there was a bug with our code that divides by 0 and caused an exception. However, because we were mutating the object directly, we end up with an object that has an invalid state because it hit an error while transforming.

Good programming practices teaches us to use a try-catch block when dealing with code that might throw exceptions. But depending on how we deal with the exception, this half-mutated Orange will be allowed to continue with execution, potentially causing all sorts of run-time errors because the prices don’t match up with our database.

Now let’s look at what it would look like if we apply the immutable design on this following block of code. It doesn’t modify the original object, but creates a copy that we modify and return.

In this example, if someone is trying to use the orange🍊, the execution would fail right away because it’s null/undefined, which also means we can catch this value, so we can handle it accordingly. Anyone else using apple, would go about their day because it’s still an apple 🍎. This is also great because it places less burden on how we write the exception case.

States are always valid even during exceptions.

Because there are no invalid intermediate states, when exception happens, it is very easy to debug what are the states that leads to that exception. In other words, when running in debugging mode, what you see is what you get.

(3) Simpler design

The combination of (1) and (2), leads to an overall simpler design of your system. Reason being that there will be less run-time errors⁴ due to states being valid at all times.

Less cognitive load

Because you can rely on the consistency of the system, you get the side benefit of less cognitive load when reading through the code, i.e. what you see is what you get. There are less side-effects⁵ to consider. Side effects are operations that happen outside of your function. So external services, databases, or caching that are triggered outside of your function control.

Allows caching more easily

Reference to those immutable objects can be cached because you know it’s not going to change!

Summary 🔖

Immutable is a wonderful design pattern that should be applied to your code. But not everything should be immutable, there are cases where mutable variables just makes more sense. (e.g.for-loop)

If there’s one key take away from all of this, you should be conscious of what you’re making mutable.

I hope this gives a good idea of what the whole concept of immutability is, and the benefits of using it. Now, how to apply it?

Stay tuned for part 2 and 3.

Footnotes

[1]: Global variables are generally a bad idea, but it’s much simpler to provide an example with global than passing by reference.

[2]: Concurrent != parallel. Concurrency is when two tasks can start, run, and complete in overlapping time periods. Parallelism is when tasks literally run at the same time, eg. on a multi-core processor. [link]

[3]: Reasons for not adopting: It’s a “newer” pattern. Most languages default to mutations. It’s what we are taught from the start.

[4]: Run-time errors are the errors that don’t get detected by your IDE, and only occurs when a specific branch of execution occurs. Think of that if-block that runs once in a life time.

[5]: side effects are interaction with services/code/cache/db that are outside of the function calling it.

A passionate software engineer who’s interested in writing and experimenting with technology

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store