When mutability bites

Photo by Jordan Whitt on Unsplash

Some time ago in the Clojure subreddit, the question was asked, “Does anyone have any concrete examples of when mutability got them into trouble?”

The answer is yes, I have some.

A popular credit card processing library (let’s call it Foo)

A few years ago, I was using Foo in my front-end. I had my own custom credit card form whose state was stored in a nice, immutable Redux store. I passed the card info into the Foo library. Payments got processed. All was well with the world.

But then I noticed, if you entered a bad card (expired, say), my form’s date field would suddenly become blank. Why was that?

Upon inspection, I discovered that my credit card state had changed from this:

{
exp: '10/20',
}

To this:

{
expMonth: '10',
expYear: '20',
}

What the?!? I searched the codebase for expMonth and couldn’t find it. I double-checked my exp input field’s onChange event handler. I traced the code along every route I could think of until it took me to the Foo call. Foo was deleting a property from my object and replacing that property with two different properties that my code didn’t expect or understand. Needless to say, I was highly annoyed.

Dates in ES

In ECMAScript, dates are mutable. This makes me sad. I have often run into bugs which go something like this:

// Hey, I want to create a date range with a start and end!
const start = opts.startDate || new Date();
// The end date should default to the start date
const end = opts.endDate || start;
// Many lines of code later…
// I’m gonna shift my start date a bit…
start.setDate(start.getDate() — 1);

At some point, I notice that my end value is not what I expected… I put breakpoints or print statements at every place where end gets modified. None of them gets hit.

ARGH!!! I chuck my laptop out the window, and decide to change careers.

The next day, after a good night’s rest, I remember, “Oh, yeah. Dates are mutable in ES… Turds.”

Strings in C++

Generally, when your module exposes a public function, you validate its arguments prior to continuing. Here’s some (untested and silly) pseudocode:

const speech = {
cat: () => 'Meow',
dog: () => 'Woof',
};
function pet(kind: string) {
// Let’s validate that kind exists in our speech map
assert(exists(speech[kind]));
  // We’ll return a function which will speak like our kind of pet!
return () => speech[kind]();
}

Given that code, you might be surprised by the following behavior:

// Here, str = 'cat'
const mittensSays = pet(str);
mittensSays(); // 'Meow'
mittensSays(); // 'Meow'
// Do lots of stuff…
mittensSays(); // Uncaught exception!

How could that have happened? We verified that kind was valid, and it’s captured in a closure, so no one has access to it! Calling mittenSays used to give us 'Meow'. How could it later throw an exception?!?

When you read that code, you probably assumed that strings are immutable. Strings probably are immutable in your language of choice. But back in the day, I did my fair share of C++ programming. (I was never very good at it.) I used to regularly mutate strings. So, if you passed a string to my upperCase function, I’d directly convert your string to upper case by mutation, rather than by creating a copy.

The way the previous example failed was that someone overwrote values in str like so:

str[0] = 'B'; // Making `str = 'Bat'`

That sort of thing gave my C++ programs really difficult to diagnose issues like the one in the mittenSays example. And while strings are generally not mutable in other languages, you can get yourself into similar messes by mutating arrays and objects.

Multi-threading headaches

I (foolishly) used to write my own multi-threaded C# servers. These days, I leave such things up to super nice OSS products written by smarter developers than I. But earlier in my career, I did loads of my own multi-threading logic. I won’t go into specifics, but suffice it to say, mutation + multi-threading is not your friend. Debugging exceptions, race-conditions, and deadlocks is a gigantic pain. Most (but not all) of my multi-threaded problems were due to sharing mutable state between threads. Eventually, I learned my lesson and moved away from shared mutable state to something akin to the actor model. Basically, as often as possible, I coordinated threads by passing immutable messages between them. I reserved shared mutable state to very few, tightly controlled constructs.

Debugging components

Some of the trickiest UI bugs that I’ve had to deal with came from scenarios where an object was being mutated somewhere, but I couldn’t figure out where or why. This is similar to the Foo credit card processing example. It used to be common that I would debug a UI component in my application, and notice that its internal state had a weird, unexpected value. After much digging, I’d discover that some portion of my internal state was actually shared with some other misbehaving component. In my old Angular 1 apps and my old C# apps, this kind of thing drove… me… crazy… Clojure(Script) and/or React + redux, with their emphasis on immutability, have largely put this pain behind me.

Unit testing

I write tests like the following all the time and then have to go back and correct myself:

test('appendTitle only modifies the titles property', () => {
const prevState = {
series: 'Lord of the Rings',
titles: ['The Fellowship of the Ring'],
};
  const nextState = appendTitle(prevState, 'The Two Towers');
  assert.equal(nextState, {
series: prevState.series,
titles: ['The Fellowship of the Ring', 'The Two Towers'],
});
});

Here, I’m trying to test that appendTitle appends a title to the titles array, but leaves the rest of the object unchanged. The test looks relatively straightforward, right? But the test passes even with the following implementation of appendTitle:

function appendTitle(movieSeries, title) {
movieSeries.series = 'MWAHA HA HA HA I CHANGED IT, SUCKA!';
movieSeires.titles.push(title);
return movieSeries;
}

Whoops!

Parting thoughts

Language designers have (generally) realized that strings should be immutable by default. Most language designers have also realized that dates should be immutable by default. But somehow, most language designers haven’t taken that logic one step further and applied it to all data. In the most popular programming languages, objects, maps, lists, etc are mutable by default. This generally means lots of defensive copying or lots of subtle bugs or both.

When I write code, I’m generally only thinking about a narrow path through my codebase. I’m thinking about the very specific problem that I’ve set out to solve. In the past, when I mutated data, it was usually under these conditions. I assumed that my code owned the data being modified, and that it would only be used in this specific scenario. I didn’t consider the possibility that someday, my data might be shared between various components / objects / functions / whatever. And so, I introduced subtle bugs.

These days, whenever I mentor students, I advise them to reserve mutability for two scenarios:

  • performance optimizations (rare)
  • local construction of data (less rare)

What I mean by “local construction of data” is when you’re building up a new data structure that is entirely owned by your function (for example, when you’re performing a reduce over a list, the accumulating object should be entirely local). Here, I still prefer immutability, but depending on the language, it may be clearer (and is almost always more performant) to mutate the data structure that is being built. Since the data is entirely local, the mutations will have no outside side-effects and are fairly easy to reason about due to the locality of the code. Once the data has been returned / is done being constructed, I consider it to be immutable.

Over time, my personal habits have shifted — as have my language preferences. Today, as much as possible, I try to avoid writing mutable code. I also now strongly prefer languages that are immutable by default.