Immutability we can afford

Photo by Li Yang on Unsplash

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

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

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

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

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 the world with fast and cheap machines, we can afford costly abstractions. In the 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

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

Other trends in programming languages

Project Lead for the Kotlin Programming Language @JetBrains