DEV Community

loading...
Cover image for How to write IMMUTABLE code and never get stuck debugging again

How to write IMMUTABLE code and never get stuck debugging again

dglsparsons profile image Douglas Parsons Originally published at dgls.dev on ・5 min read

I've written production code in a variety of different languages throughout my career, including Haskell, Scala, Go, Python, Java or JavaScript. While each language has its own clear benefits, working as a polyglot across a range of different paradigms has changed the way I write code. Certain skills and concepts are transferable regardless of the language being written. I believe immutability is one of these key concepts. By writing immutable code it is possible to make programs easier to reason about, easier to write and easier to debug.

Here, we’ll look at three things:

  • how walruses eating cheese can explain how immutability works,
  • why you should care, and
  • why the counterarguments against immutable code aren’t worth considering.

What is immutability? #

“unchanging over time or unable to be changed.” - Oxford Languages definition.

Immutability is the idea that once an object or variable has been created, its value should never change or be updated by anything. For objects or classes, this also includes any fields; literally, nothing should change! The object is effectively read-only.

Writing code in this style requires a mindset shift at times though. The first time I came across the idea, it made absolutely no sense to me and seemed insane. I was confused and wanted to immediately unpick it all, writing it in a way I was familiar with. Gary Bernhardt, in his talk on boundaries, gives a fantastic example of why it feels so wrong.

He talks about feeding walruses cheese.

Walrus

In a mutable version, we might instruct each walrus to eat some cheese. This cheese then gets added to the contents of their stomach. Makes a lot of sense, right?

In an immutable version, we have to perform a mind-bending operation. To feed the walruses we would have to:

  • create a brand new stomach that’s the same as the old stomach, but with some cheese in it.
  • Then, create a new walrus that’s the same as the old walrus, except, with the stomach replaced.
  • Then, throw away all the old walrus.

At first glance, this sounds bonkers but stay with me - let’s look at what makes writing code like this worthwhile.

How does it prevent pain when debugging? #

Have you ever encountered:

  • undefined is not a function in JavaScript?
  • NullPointerExceptions in Java?
  • SegFault in C/C++?
  • panic in Go?
  • NoneType has no attribute foo in Python?

If you’ve worked in any of these languages, then chances are you probably have. The thing is, all of these errors are caused by the same thing: missing, or null, data.

Missing data and null values are definitely among the most difficult types of bugs to track down and fix. I’ve spent countless hours in the past sifting through JavaScript code trying to figure out why the value I thought should be there, wasn’t. Why my application suddenly crashed when everything seemed to be going fine. Sir Tony Hoare even describes null as “The Billion Dollar Mistake” because of the countless bugs, security vulnerabilities and crashes that have resulted from it.

Let’s just agree: nulls can be evil.

The reason these bugs are so hard to hunt down and to fix is that the effect (the exception) is far away from the cause (the introduction of null). Actually throwing a null pointer error happens some arbitrary amount of time after we introduce a null, and we get undefined errors accessing a property miles away from where we thought the property was set. Debugging becomes a case of reading carefully back through code until we find the cause.

The more state changes that happen in code, the more places these bugs can be introduced. Instead, we can attempt to reduce the surface area of any code. The fewer mutations in a codebase, the less surface area there is for bugs. This leads to fewer bugs.

If you only ever set a value once, there’s only one place that value can be faulty. If you make changes to an object as it gets passed around, any one of those places could introduce potential issues. If one of our walruses is faulty, we know it can only have happened when we made the latest walrus, complete with the new stomach. It can’t be an issue with an earlier walrus - they are long gone.

So really, immutability, or, never changing a value, really saves us from getting stuck debugging.

Why performance isn’t a concern #

Some eagle-eyed people might be thinking “those walruses earlier… isn’t throwing them all in the bin and making new ones pretty expensive? Won’t it make my code slow?”.

The answer isn’t simple.

You’re right in saying that throwing away walruses all the time isn’t totally necessary, and it can make things the tiniest amount slower sometimes. The keyword being sometimes here though. Sometimes compilers are clever enough to optimise this behaviour with something more efficient. Some languages even prefer immutability by default. Immutability also has great benefits when it comes to multi-threading or parallelisation, as it allows lock-free sharing, knowing that values won’t be changed.

Despite all this, even if creating new walruses is slower in the language you use, the cost of allocating a new object is almost certainly minuscule compared to anything else within an application. Unless you are benchmarking and actively measuring performance, then you almost certainly shouldn’t care.

Conclusion #

Immutability is a powerful tool when programming. It allows us to write code that is easier to debug and reason about. It requires a bit of a mindset shift, but in my experience, it’s definitely worth making the mental leap.

Give it a go, and let me know what you think :).


Looking for other ways to improve the clarity of your code? Why not check out my post on never using else statements.


Enjoyed this post? Want to share your thoughts on the matter? Found this article helpful? Disagree with me? Let me know by messaging me on Twitter.

Discussion (25)

pic
Editor guide
Collapse
winstonpuckett profile image
Winston Puckett

I totally agree. And to tack on to your point about immutability and performance, I've found that strange database queries are consistently the reason for slowness in my app. I look forward to the day when I'm at all concerned about how the code chooses to create and destroy objects

Collapse
dglsparsons profile image
Douglas Parsons Author

Is that using an ORM?

Collapse
winstonpuckett profile image
Winston Puckett

Hahaha... And there lies the problem with ORMs.

It is often more that database queries are repeated in odd spots when they don't need to be. It is sometimes the ORM's fault, but I like the Linq syntax so much I don't want to blame it all on that

Thread Thread
dglsparsons profile image
Douglas Parsons Author

I've not used Linq much, so I suspect I don't know what I'm missing out on. I'm a big fan of NoSQL though, partially because it doesn't let you query in inefficient ways.

Thread Thread
winstonpuckett profile image
Winston Puckett

I didn't know that. That's pretty cool

Collapse
vlasales profile image
Vlastimil Pospichal

ORM does not use immutability and that is why there are such problems with it.

Collapse
carlyraejepsenstan profile image
CarlyRaeJepsenStan

I liked the walruses analogy! Gave me a good laugh.
If you don't mind my noob question, how would one iterate over an array/collection/vector, to create 100% immutable code? The usual loops would not be an option, as the iterator or counter variable needs to change.

Collapse
bytebodger profile image
Adam Nathaniel Davis

The answer is: recursion. In true Functional Programming (which heavily features immutability), there are no loops. People throw around terms like "functional programming" and "immutability", but they rarely think about what it takes to fully implement these features.

Everything that you can do with a loop, you can also do with a recursive function. Here's a simple example:

const countToTen = (iterator = 1) => {
  if (iterator > 10)
    return;
  console.log(iterator);
  const nextIterator = iterator + 1;
  countToTen(nextIterator);
}

countToTen();
Enter fullscreen mode Exit fullscreen mode
Collapse
carlyraejepsenstan profile image
CarlyRaeJepsenStan

Wow, thanks so much! This is exactly what I was looking for.

Collapse
dglsparsons profile image
Douglas Parsons Author

Hey, I'm glad you enjoyed the article <3.

That's a great question and thanks for asking. You're absolutely right about the iterator or counter variables changing. It's not something you can avoid really as it's so inherent to how loops work.

I don't have particular problem with that though (although in some languages you have to be careful, especially if writing asynchronous code that uses those variables). What's more important, in my opinion, is what you are doing in the iteration - are you mutating an array in place, or returning a new array? Using map and reduce where possible helps a lot.

Hope that helps!

Collapse
carlyraejepsenstan profile image
CarlyRaeJepsenStan

I see - that question has bugged me a lot while I'm coding. Thanks for the advice!

Collapse
mrxcitement profile image
Mike Barker

Here is an interview with Robert (Uncle Bob) Martin talking about the book "Structure and Interpretation of Computer Programming" and his and the books take on immutability and its benefits. youtu.be/Z0VpFmp_q4A?t=148

Collapse
dglsparsons profile image
Douglas Parsons Author

That's definitely one of my favourite programming books of all time. Packed full of wisdom. Really interesting video and a great take on functional programming too! Thanks for sharing this.

Collapse
mcsee profile image
Maxi Contieri

Amazing Article!

Immutability is the only way we can guarantee code stands time.

I'll give you some pointers to follow:

maximilianocontieri.com/the-evil-p...

and NULLs that should be avoided as you clarified

maximilianocontieri.com/null-the-b...

and this article you point out is also excellent

doc.rust-lang.org/book/ch03-01-var...

We should push for MORE immutability on our objects

Collapse
dglsparsons profile image
Douglas Parsons Author

Thanks for the useful links, and glad you agree :).

Collapse
eecolor profile image
EECOLOR

Unless you are benchmarking and actively measuring performance, then you almost certainly shouldn’t care.

Yeah! The only situations I know I am using mutability is when I have to process tens of thousands of items, like this:

const index = array.reduce(
  (result, x) => (result[x.prop] = x, result),
  {}
)
Enter fullscreen mode Exit fullscreen mode

If it is below tens of thousands I will write this:

const index = array.reduce(
  (result, x) => ({ ...result, [x.prop]: x }),
  {}
)
Enter fullscreen mode Exit fullscreen mode

My rule is: if mutation makes the code significantly better to read or actually helps with performance, you can apply it locally.

So in code reviews I ask people to move stuff into functions that, from the outside, seem to be immutable:

function replaceAt(array, index, x) {
  const copy = array.slice()
  array[index] = x
  return copy
}
Enter fullscreen mode Exit fullscreen mode

The alternative would be something like this (more likely to have errors):

function replaceAt(array, index, x) {
  return [...array.slice(0, index), x, ...array.slice(index + 1)]
}
Enter fullscreen mode Exit fullscreen mode

Side note: if a language has an immutable construct in it's standard library I would prefer that.

Collapse
vsingh7 profile image
Vikram Singh

Great article! I've only heard about immutability in passing, but never really looked into it. Your post has me intruiged! Where can one go to find out how to write immutable code in their language of choice? I know I could easily Google this, but if you have some resources off the top of your head, I would greatly appreciate the direction :)

Collapse
dglsparsons profile image
Douglas Parsons Author

Hi Vikram, I'm glad you enjoyed the article and it's fantastic that you are intrigued!

For specific resources - I sadly don't have any off the top of my head. Personally, I feel like it's a change in approach to writing code as much as anything else.

For Javascript and Python, I'd definitely have a look into the spread operators though (for objects / dicts), and spreading in arrays for javascript.

Collapse
greenroommate profile image
Haris Secic

Using it mainly to avoid DATA corruption or in other words classes that represent data models are mostly immutable in my code.

State I like. I love being able to put something in a service (class or module or whatever the term in given language) where it only changes itself with all the restriction in place. There is no setter put things like next() or such which heavily do verification or so. This has saved me loads of time, shortened my code, made it more readable and clean and in one specific case made it more safe to multi-thread as it throws exception on unexpected behaviour which informs me that something else is trying to hit the data which I never expected it to do so. So basically in some ways it actually was crawler through code which would detect unintended thread switching with parallelisation. But regardless of the last one-time specific case I see no point in preventing state anyways. My connection drivers & driver pools have state and I expect them to do so to save some networking overhead. My registries for active objects (like special services) have such state and I love it. You could reset settings in runtime making code drop all active services and create new ones with the new properties sent via let's say HTTP - services may be immutable in that case but registry is not so again the state and mutability. There's plenty of reasons to use it and not bother yourself with monad, monoid, hemorrhoid...

I think people abused it too much so they're running away from it as much as possible (but also yeah theorist and their maths and such for which some of us don't care). Some combination of both worlds is always better in my books. It's like when you learn to use less memory and start hitting short or such in every language until you realise int actually works faster in some environments because it's running on x86-64 which has to do splitting so you have to pick memory over processor. Then you realise hey it's good we have both.

Collapse
devoresyah profile image
DeVoresyah ArEst

always use immutability in every single of my react native project 🎉🎉🎉

Collapse
dglsparsons profile image
Douglas Parsons Author

Glad you find it useful. Javascript is so much nicer when you write immutable code. There are too many foot-guns otherwise!

Collapse
sebastienlorber profile image
Sebastien Lorber

Interesting metaphor 🤪

I don't really understand the relation between nullpointer errors and mutations, that's worth expanding a bit with an example where a mutation does lead to such error.

Collapse
abdadabda profile image
abda dabda

An interesting read, though I still can't understand one thing.
Instead of modifying already existing objects, we decide to throw them away and create new ones. This is fine. How does it help us with avoiding bugs, or ease debugging? We still have to populate a new object, possibly with values coming from somewhere, possibly from previous objects. There still can be some nulls etc, we still can get errors, and we still can have a situation when a value was introduced deep in the execution tree. I know that I possibly have some misconception or I understand the topic poorly, so I would be glad if this could be elaborated :)

Collapse
bpedroza profile image
Collapse
andreidascalu profile image
Andrei Dascalu

I always dislike the formulation "immutable code". Depending on understanding, code is either always mutable or always immutable.
But here we are talking about the data that the code manipulates.