Immutability we can afford
At the dawn of software engineering computers were programmed directly in machine code, then in assembly, and only later in higher-level languages. Computers are imperative. They operate by executing instructions that mutate their state, stored in registers and memory. Naturally, the same was true about programming languages. In the old world of expensive computers with limited resources, the primary concern was an efficient translation of higher-level abstractions into low-level code.
The past glory of mutable state
It used to be a normal practice to write software in a way that mimics the actual computer architecture with thousands of global variables that are being mutated by various pieces of the system. It might be shocking for a modern developer to learn that just recently there were cars on the street, designed as late as 2005, that ran what we would call the “Spaghetti” code.
The software industry, in general, had firmly moved past the unruly global state before the end of the last century. The rise of the object-oriented programming paradigm had established an orderly approach with the encapsulation of all the mutable state in our software systems inside of objects. It had fueled tremendous growth in the complexity of modern software, layering abstractions above abstractions, while still maintaining a reasonable degree of human’s ability to make sense of it.
However, any developer who worked on a non-trivial piece of UI using an object-oriented framework, or had programmed in another domain with lots of asynchronously occurring events, can tell you stories of debugging all those cases where mutable state, even encapsulated into objects, continually trips you. You expect this object to be in a such and such state, but due to some rare sequence of events, it turns out to be in a state you did not expect, having been mutated by another piece of code.
For example, take a popular architectural pattern where a repository class encapsulates a piece of application state and sends updates of this state to other layers of application:
class Repository {
private val state = State("some state", 42, ...)
}
If State
is represented by a classic object-oriented mutable object, then developers have to be careful when sending it around through any kind of asynchronous processing pipeline or event bus, as a receiving code might discover it in a different state. Even caching such states becomes a challenge. A discipline of defensive copying is needed to work around that.
Immutability takes the stage
A solution is simple on the surface — embrace immutability. Immutable objects are incapable of surprising a programmer by an unexpected change of their state.
Yet, historically, immutability has been a prerogative of niche research languages. Mainstream languages provided support for immutability as a 2nd class citizen, if at all. Just look at basic signs. In most programming languages from the past century a declaration of a normal integer field of an object, like int x
, means mutable variable by default, while extra effort is needed to make it immutable by adding a modifier like const
or final
. If you look farther back in time, even text string types used to be mutable, like in C (1972).
However, many modern languages treat immutable on par with mutable (var
vs val
/let
having the same length by design) at least for simple data, or even prefer immutability by forcing developers to go through extra hoops when declaring common data containers as mutable (like List
vs MutableList
).
A picture worth a thousand words. Pick high-level languages that enjoy popularity nowadays, check whether they were designed in the 20th or the 21st century, and arrange them according to their basic degree of respect for immutability:
As you can see, among the languages designed in the 20th century, only Haskel (1990) stands apart, being wholly based on ideas of immutability. In the 21st century, immutability gets a lot more love with Scala (2004), F# (2005), Rust (2010), Kotlin (2011), and Swift (2014) treating it at least on par with mutability for variables.
The cost of immutability
The reluctance in accepting immutability is not surprising. It is a radical and costly leap of abstraction from the underlying mutable machines. Most modern software is interactive. It works in a changing environment. Representing a non-trivial state (beyond a simple type that fits into a machine word) by immutable values means copying these values on each update.
All data structures and algorithms that we use in day-to-day work (like collections) can be implemented as immutable, yet their most efficient implementations are typically asymptotically slower than their mutable counterparts, meaning that they become progressively slower when they work with more data. Even with a small number of data items they run several times longer than the corresponding algorithms with mutable data structures.
We can afford abstractions
A lot of developers nowadays still program constrained systems or at such a massive scale where every bit of efficiency is important. This creates a considerable market for zero-cost abstraction, systems programming languages. However, if you look at a larger picture, at the kind of code that millions of developers write every day, you’ll inevitably find that the concern for resource consumption often takes backstage. With so much demand for software in the modern world and with lagging supply of developers to meet it, the frontstage concern becomes developer productivity.
There is an incessant need to equip programmers with abstractions that let them get their job done faster, with less effort. These abstractions are costly, but it does not worry a majority of people writing code today. In a world with fast and cheap machines, we can afford costly abstractions. In a world with an expensive developer workforce, programming languages are racing to provide convenient abstractions. Immutability removes the whole class of problems and concerns in software and we can afford immutability nowadays.
Beyond programming languages
A course to immutability is clear even beyond the design of programming languages. Take a look at UI frameworks. They all used to be wholly object-oriented in their philosophy. Developers used to have a tenuous responsibility of carefully writing code that mutates states of UI objects to make them match the data that needs to be displayed. This UI programming style is going away. Reactive UI frameworks that are based on unidirectional data flows are growing like fire everywhere: React (JS, 2013), Compose (Kotlin, 2019), SwiftUI (Swift, 2019) to name a few. A great overview of problems that these UI frameworks solve and the whole class of boilerplate code they remove can be found in Leland Richardson’s talk “Understanding Compose”.
What is common between those modern UI frameworks is that most of them chose to work with immutable representations of data that is being displayed. Attempts to build better observability and data-binding for changes inside complex mutable objects are being abandoned. This trend also fuels immutable data pipeline abstractions like the Reactive Streams and Kotlin Flow.
Conclusion
Immutability is a luxury of programming, making software more comfortable and less error-prone to write in many important cases. Immutability is becoming more popular in software development because we became so rich in machine resources that we can afford it now.
Other trends in programming languages
If you are interested in trends that shape the design of modern programming languages, then check out my other related stories: