We’re continuing our series on Tiny’s programming principles. We’re covering the rules, guidelines, and foundations we set for our developers to help them create more stable, usable, and effective code.
Immutability is one of our four main programming principles, and we believe every developer should know about it (and try to implement it in their work). So, let’s talk about the concept of immutability, why you should do it, and how to apply it to your own projects.
Mutable vs immutable: what is immutability?
So, what is immutability? To answer that, let’s define what it is and isn’t.
In programming, mutable and immutable refer to the state of an object - and how variables can be assigned and changed. Data needs to be changed - after all, most sites and applications these days are dynamic - but how that data is changed is what matters.
Mutable objects can be modified (or mutated) after they’re created, and transform into other data or variables. For example:
// lowercase user data stored in an object by mutating the values
const user1 = { firstname: ‘John’, lastname: ‘DOE’ };
console.log(user1); // logs { firstname: ‘John’, lastname: ‘DOE’ }
const lowerCaseUserData = function(user) {
user.firstname = user.firstname.toLowerCase();
user.lastname = user.lastname.toLowerCase();
}
lowerCaseUserData(user1);
console.log(user1); // logs { firstname: ‘john’, lastname: ‘doe’ } - has changed
Whereas, immutable objects are unchangeable. They’re like a statement of fact or a source of truth. The data or variable will remain the same. To work with new data, you’d need to make a copy of the object and use the copy from that point onwards. For example:
// lowercase user data stored in an object by creating a new object
const user1 = { firstname: ‘John’, lastname: ‘DOE’ };
console.log(user1); // logs { firstname: ‘John’, lastname: ‘DOE’ }
const lowerCaseUserData = function(user) {
const firstname = user.firstname.toLowerCase();
const lastname = user.lastname.toLowerCase();
return {
firstname,
lastname
};
}
const lowerCasedUser = lowerCaseUserData(user1);
console.log(user1); // logs { firstname: ‘John’, lastname: ‘DOE’ } - hasn’t changed
console.log(lowerCasedUser); // logs { firstname: ‘john’, lastname: ‘doe’ } - new object with changes
What are the benefits of immutability?
Mutation can cause some pretty strange things to happen…
If you’ve ever run into errors because your code or state is mutated, you might already have an understanding of why immutability is better. Some of the main benefits of immutable objects include:
- Improved readability - Because variables stay the same, code is simpler to understand and reason about (without a whole lot of commenting)
- Improved efficiency - While it might take a little more effort and consideration to write immutable code, in the long run, it becomes quicker to update and more efficient to manage
- Traceability - Easily trace how and where your data is changing in the code, and determine what parts of the application need to reload with each change
- Improved safety - Once you’ve verified the state of an immutable object, you can be confident that it will stay safe and valid (no background process will be able to change it without you knowing)
- Better caching - You can cache references to immutable objects since they won’t change
- Easier testing and debugging - Since the state of your immutable object doesn’t change, it’s much easier to test your code, and track down bugs
Immutability and functional programming
Like most of our programming principles here at Tiny, our preference towards immutability comes back to our love of functional programming and the theory behind it. Functional programming is a different way of thinking about programming (at least, compared to how many programmers think and how they’re taught at university).
Functional programming has a concept of "pure" functions. These are functions that avoid any kind of "side effect", avoiding code within a function that affects things outside the function, often via mutation. There are many arguments as to why pure functions are preferable. For example, it’s easier to read and test code when a function's functionality is contained within it entirely. It’s also easier to avoid bugs that might crop up because of code unexpectedly modifying variables elsewhere, which can have a butterfly effect.
Functional programming is based on math so that it’s easier to reason about the code, it’s more stable, and less likely to fall apart. Immutability is a big part of that because if objects are mutable and could have a different value than expected, the math isn’t always going to add up. So, in functional programming land, if you want to change the value of something (like a variable), you always make a copy of it and then deal with that copy from then onwards.
In fact, many of the languages more dedicated to functional programming, like Scala or Haskell, usually won’t let you mutate things because it’s considered too dangerous. In instances where mutation is necessary (like with user input), they have a very clearly defined way to approach it. Of course, most of the time, it’s up to you to enforce immutability, especially if you work with JavaScript.
Why immutability is important in JavaScript
JavaScript, by its nature, makes it very easy to mutate variables. You can even mutate prototypes, including the built-in prototypes such as Array and Object.
Other languages tend to have features like required keywords that clearly distinguish mutable and immutable variables, or restrict what you can mutate (for example, just the variables you've created). These features and restrictions make it more difficult to mutate something without meaning to.
But JavaScript makes it very, very easy to mutate pretty much anything - and even some of the built-in language features behave certain ways because they use mutation internally. Unfortunately, mutation is the source of a lot of bugs in JavaScript web apps.
A common problem is that you’ll have a value somewhere in your code (say, a number), and you call a function and it mutates that number. But then you’ll have another bit of code further down which also uses that number. The problem with this is that it doesn't realize that function got called, so it assumes it’ll be a different number to what it actually is.
Here at Tiny, we've had bugs like that before.
For example, we had a helper function that we thought took an array and a function, applied that function to each item in the array, and returned a new array with the new values. Then we received a bug report that something was failing. When we looked into it, we realized an array of values was being mutated in a way that it shouldn’t have been. We eventually tracked it to an earlier point in the code where we thought we were making a copy of the array and changing that copy - using the helper function mentioned earlier. It turned out that our helper function was actually mutating the array that was passed in and returning that, instead of creating and returning a new array.
Here’s a pseudocode example of what happened:
// this is the helper function that we didn’t know was mutating the input array
const map = function(arr, f) {
for (let i = 0; i < arr.length; i++) {
arr[i] = f(arr[i]);
}
return arr;
};
const xs = [‘1’, ‘2’, ‘3’, ‘4’, ‘5’]; // the original array
// many lines of code…
const newXs = map(values, parseInt); // mutates xs and returns the array, so xs === newXs
// many more lines of code...
someFunction(xs); // this function failed because it expected the original xs array not the mutated version
The difficulty in tracking down this bug was that it wasn’t being caused where the code was failing. We had to track the array that contained the wrong values back quite a way before we found where it was accidentally being mutated. It was time-consuming to find, especially considering how small the problem was, and could have been avoided entirely if the helper function didn’t use mutation.
JavaScript arrays and mutation
Mutability is built into JavaScript in many ways.
For example, Live Collections (which are lists of nodes or elements) are mutable. This encompasses any JavaScript method that returns a HTMLCollection or Live NodeList, as these data types auto update when the DOM changes. In fact, a NodeList can be “live” or “static”, which means it can be tricky, because unlike HTMLCollection where you know it’s live, NodeList could be either. This includes:
- GetElementsByName - Returns a Live NodeList Collection of elements in the document with the given name
- GetElementsByTagName - Returns a Live HTMLCollection of elements that have the given tag name
- GetElementsByClassName - Returns a Live HTMLCollection containing every descendant element with the given class name or names
Live Collection methods are mutable because their value changes to reflect changes to the elements on the page. If you do something to update the DOM, like adding or removing elements or changing text, the Live Collection will update to reflect those changes. This can make development difficult, as you may not be able to completely control when the DOM changes and your list of elements might change underneath you during computations, causing unexpected side-effects. Depending on how you handle this, it could also lead to a poor user experience.
Rather than Live Collections, it’s often better to choose an immutable alternative, like Element Arrays. That way, you can get the arrays, clone them, make your changes on the cloned set of elements (that look the same as the live version), and then insert them back into the page, replacing the original elements.
Many of the JavaScript methods for getting elements from the DOM return live arrays/collections, so you can't always avoid them.
Built-in JavaScript methods that use mutation include mutating array methods, variable declarations, void functions, map mutator methods, set mutator methods, and mutating function arguments. Watch out for:
- add
- clear
- clear
- copyWithin
- delete
- delete
- fill
- let
- pop
- push
- reverse
- set
- shift
- sort
- splice
- Unshift
Side note: Tiny has helper functions in our Sugar library for getting elements (similar to JavaScript's built-in methods like getElementsFromTag) that are specifically designed to avoid Live Collections. Instead, we make arrays of the elements. Want to try this approach in your project? Check out Sugar here!
Immutability and modularity
Mutability also links in nicely to the idea of modularity. With modular programming, you have "modules", or distinct pieces of code that deal with a piece of functionality in isolation.
If you allow mutation across modules (at whatever level, whether it be functions within files, files within folders, or folders within a library) you break down that distinction between modules. As a result, you lose some of the benefits associated with modularity, like readability and testability.
So, if you’re going to do modular programming, you should also follow the principle of immutability.
If you’d like to learn more about modularity, check out my previous blog on modularity and why it’s another programming principle we follow at Tiny.
6 common reasons immutability isn’t universal
If immutability is so beneficial, why doesn’t every programmer work this way? Why don’t we make everything immutable?
1. Lack of awareness
For a start, not everyone knows about immutability - yet. (So please do your fellow developer a favor and share this blog!)
2. Laziness or time constraints
Many programmers continue to use mutable objects because they’re short on time (or lazy). Being lazy can be a good thing as a developer - you might find a quicker/smarter way to do something. And if you’re prototyping, you might just want to get things done as quickly as possible. But using mutation to speed up the process may not pay off, as you’ll likely lose more time later on if you need to track down and fix fiddly mutation bugs. So, make sure you weigh up the pros and cons of both.
3. Convenience
Like most of the programming principles we follow at Tiny, not doing them can be quicker, easier, and more convenient (at least, to start with). Mutability often means less code and fewer variables floating around that you have to name (and naming things is always hard). But the extra time and code is worth it for the benefits in the long run.
4. Not possible
Unfortunately, it's not always possible to avoid mutation, especially when dealing with user input or UI modifications. For example, dynamically changing the HTML of a page when a user clicks a button requires mutation. Plus, some of the built-in parts of JavaScript, like many of the Array methods, use mutation, and there isn’t always a viable alternative option.
5. Speed is your top priority
There are some scenarios where mutable objects just make more sense. For example, rendering speed could be your #1 priority - perhaps you’re a game developer or you make software for the stock market with real-time dashboards. In that case, mutating values instead of copying them each time there’s a change may help your interface run faster.
6. Simple/small programs
In small apps and programs that aren't too complex or conscious of things like security and performance and robustness, the risks associated with mutation might not have enough of an impact to worry about.
So, as you can see, mutation is sometimes helpful or even necessary. However, for most situations where you could use mutation, there are often more cons than pros. That’s why my team and I avoid it as much as possible.
Why it’s important for the TinyMCE project
When your code reaches a certain size and complexity - and when you care about security, performance, and robustness - mutation and the resulting risks are something you should carefully consider.
This is why, from early 2016, we began taking steps towards using functional programming in the TinyMCE codebase. Part of this move meant leaning further into the principle of immutability. This journey culminated with a huge jump over to TypeScript in December 2017.
Side note: TypeScript has a readonly type modifier which we’ve started using in our code fairly recently. It protects against accidental mutations by preventing the value of properties from being changed, else the compiler will throw an error. If you’re considering a move to TypeScript, you might find this feature useful!
Back to mutation and big projects…
TinyMCE is big enough that unintended mutation can have far-reaching impacts. For example, there are places in our code base where if we accidentally mutate a variable, it could affect most (if not all) of the rest of the main code base. That could cause a devastating bug. It could also be very hard to find what's causing a bug if it's caused by mutation, and sometimes the fix isn’t simple because some parts of the code might rely on the mutation while other parts need to not use mutation.
The principle of immutability has been critical to helping us create a more stable, secure product with fewer bugs, that’s also easier to read, test, and debug 🙌
Get Your Free TinyMCE API Key →
How to start implementing immutability
If you and your team want to create more stable, easy to read code, with fewer bugs, it’s a really good idea to start implementing immutability. Here’s a good place to start:
- Chat to your team - Start a conversation about some of the problems you’ve noticed that were caused by bad mutation
- Educate - Explain the concept of immutability (send them this article, if you like!) and offer training and examples so they understand what immutable objects should look like (and what mutating functions to avoid)
- Plan - Make a plan to start changing your practices with an upcoming project/update
- Test - Track your results, see if you get fewer bugs, and talk to your team to see if they find the code easier to understand and update
- Rollout - If immutability has a positive impact on your team’s workflow and product, roll it out broadly across future projects and updates
TL;DR
To sum up everything we’ve discussed here, the key thing for developers is to understand what mutation is and the risks associated with it. That way, you can consciously decide when to use mutation and when to stick with immutable objects. This is especially important if you write JavaScript, because of how easy it is to mutate variables without realizing it.
We’ve got more coming in this series on programming principles, so make sure you follow us on Twitter at @joinTiny and subscribe to get our weekly blog updates. And we’d love to hear from the developers in our community… so, tag us on Twitter with your thoughts on mutation - good/bad/neutral?