How to write IMMUTABLE code and never get stuck debugging again
Douglas Parsons

Douglas Parsons @dglsparsons

About: Lead Engineer @ Shamaazi • AWS • Go • React • Terraform • Posting nonsense. Writing at dgls.dev. Follow me on Twitter if you care.

Location:
Sheffield, UK
Joined:
Sep 11, 2020

How to write IMMUTABLE code and never get stuck debugging again

Publish Date: Nov 17 '20
234 26

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.

Comments 26 total

  • Winston Puckett
    Winston PuckettNov 17, 2020

    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

    • Douglas Parsons
      Douglas ParsonsNov 17, 2020

      Is that using an ORM?

      • Winston Puckett
        Winston PuckettNov 17, 2020

        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

        • Douglas Parsons
          Douglas ParsonsNov 17, 2020

          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.

      • Vlastimil Pospichal
        Vlastimil PospichalNov 17, 2020

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

  • Maxi Contieri
    Maxi ContieriNov 17, 2020

    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

    • Douglas Parsons
      Douglas ParsonsNov 17, 2020

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

  • DeVoresyah ArEst
    DeVoresyah ArEstNov 17, 2020

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

    • Douglas Parsons
      Douglas ParsonsNov 17, 2020

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

  • HS
    HSNov 17, 2020

    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.

  • CarlyRaeJepsenStan
    CarlyRaeJepsenStanNov 18, 2020

    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.

    • Douglas Parsons
      Douglas ParsonsNov 18, 2020

      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!

      • CarlyRaeJepsenStan
        CarlyRaeJepsenStanNov 19, 2020

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

    • Adam Nathaniel Davis
      Adam Nathaniel DavisNov 26, 2020

      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
      • CarlyRaeJepsenStan
        CarlyRaeJepsenStanNov 26, 2020

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

  • Sebastien Lorber
    Sebastien LorberNov 25, 2020

    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.

  • Vikram Singh
    Vikram SinghNov 25, 2020

    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 :)

    • Douglas Parsons
      Douglas ParsonsNov 26, 2020

      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.

  • Mike Barker
    Mike BarkerNov 25, 2020

    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

    • Douglas Parsons
      Douglas ParsonsNov 26, 2020

      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.

  • EECOLOR
    EECOLORNov 26, 2020

    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.

  • Andrei Dascalu
    Andrei DascaluJan 7, 2021

    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.

  • Joost Helberg
    Joost HelbergJan 20, 2022

    Great and important article, well put. I was a bit confused by the title though. I really thought that you meant code which could not change; of course it should be bug free then, as fixing immutable code is impossible. But your text is about the objects, not the code.

Add comment