Our code is infested with bugs. Yet changing one single thing could put an end to our suffering.
Before we get to the technique itself, let’s talk a little about the past of computer science.
Have you heard of the GOTO statement? It was probably the biggest cause of bugs within the early programming languages. Luckily, it is no longer used in most modern programming languages.
But little do we know that we still use its evil cousin daily in modern programming languages, with the same dire consequences.
A Little History
In the early programming languages, before the advent of functions, loops, and if statements,
goto was the only way to manage program-control flow.
Here’s how a
goto statement might have been used instead of a while loop:
In the early days, the use of
goto was one of the most common sources of bugs.
At the pre-ALGOL meeting held in 1959 Heinz Zemanek explicitly threw doubt on the necessity for GOTO statements; at the time no one paid attention to his remark, including Edsger W. Dijkstra, who later became the iconic opponent of GOTO. The 1970s and 1980s saw a decline in the use of GOTO statements in favor of the “structured programming” paradigm, with goto criticized as leading to “unmaintainable spaghetti code”.
Probably the most famous criticism of GOTO is a 1968 letter by Edsger Dijkstra called Go To Statement Considered Harmful. In that letter Dijkstra argued that unrestricted GOTO statements should be abolished from higher-level languages because they complicated the task of analyzing and verifying the correctness of programs. The letter itself sparked a debate, including a “‘GOTO Considered Harmful’ Considered Harmful” letter sent to Communications of the ACM (CACM) in March 1987, as well as further replies by other people, including Dijkstra’s On a Somewhat Disappointing Correspondence.
Prominent computer scientist Edsger Dijkstra rightly noted that “
goto statements should be abolished from higher-level languages because they complicated the task of analyzing and verifying the correctness of programs.” In other words, answering the question “how did I get to this point of execution?” was really hard when
goto was used. This typically resulted in spaghetti code, which was very hard to debug (and to understand).
The Two Pillars of Computing
Let’s first try to answer the fundamental question “what do computers do?”
Computers, fundamentally, process data. Every operation performed by a computer is always some form of data processing. Therefore, we have data and code that processes that data. A program gets some data as input and outputs some other processed data. Whenever data is loaded into memory, it is called the program state. All of the object fields and variables are state. All of the arrays, dictionaries, and maps are also part of the program state.
Code and state can rightfully be called the two pillars of computing.
First, let’s talk about code.
In the olden days, the use of
goto was frowned upon because it was hard to tell how you got to a certain point of execution. Thankfully with the structured-programming revolution, loops, functions, and if statements got introduced. This made following the logic of a program much easier, which subsequently made answering the main question of “how did I get to this point of execution?” much easier.
What about the second pillar, state? This is where things get interesting. In a typical program, can one easily answer the question “how did I get to this particular state?”
Not really — it’s not uncommon for programmers to spend hours, or in some cases even days, debugging tough bugs. The time is mainly spent trying to answer the question “why has the state changed?”
Can you relate? I still remember the agony of debugging some object-oriented programs — spending days to fix a tough bug, only to have things break elsewhere.
Goto’s Evil Cousin
Hopefully, I was able to convince you being able to answer the question “how did I get to this particular state?” is very desirable, just like being able to answer the question “how did I get to this point of execution?”
malloc in C). Garbage collection is one of the greatest advancements in computer science. Not having to worry about manual memory management (and memory leaks) is a huge productivity booster.
However, mainstream programming languages still provide no built-in way to enforce state-management constraints upon the developers. This makes it very easy to shoot oneself in the foot with state. Just like it was very easy to shoot oneself in the foot with
gotos in the olden days.
How do we shoot ourselves in the foot with state?
Typically, any function in the code base can alter any part of program state. This gets even worse with programming paradigms like object-oriented programming where there’s little to no built-in restriction on what objects/methods can affect the internal state of other objects.
A simple example
Let’s take a look at a simple example:
Line 11 prints:
Before: [3, 5, 4, 1, 2, 9].
Line 15 prints:
After: . With no numbers in sight.
Surprised? This is mutable state in action.
Here’s the implementation of
joinNumbers used above.
numbers.shift method on line 4 has actually mutated the
numbers array that was passed in. The
numbers.shift method returns the first element in the array and actually removes the value from the original array, leaving the array empty at the end.
Such a bug might not be that hard to catch in a simple program. However, as programs grow bigger, as their state gets more complex, debugging becomes much more complicated.
Not made for concurrency
Mutable state has one other major drawback. It doesn’t work well with concurrency. Nowadays, processors aren’t getting any faster. We’re about to hit the limit of Moore’s law. Processors mainly become more powerful by having more processor cores. Therefore, we have to design our programs to make good use of multiple cores. And mutable state isn’t our friend when things come to concurrent programming.
What makes it so bad?
Whenever multiple cores all access and mutate the same piece of shared memory, they’ll inevitably step on each other’s toes, resulting in race conditions. The way this has typically been solved with mutable state is by making use of constructs like locks, mutexes, monitors, etc. Waiting for threads to synchronize actually degrades performance when multiple cores are involved. It also significantly increases code complexity — while also making debugging much harder.
Programming with mutable state is also called the “imperative programming paradigm,” which was created in the era of single-core processors. In the past, memory was very limited, and writing programs in a way that reused as much memory as possible was the only choice (e.g., with shared mutable state). Nowadays, we have much more RAM available for our programs, and writing programs in a way where everything is shared is no longer necessary (and, in fact, is no longer reasonable).
The need for discipline
Mutable state can rightfully be called an “undisciplined approach to state management.”
This is one of the reasons for the growing popularity of state-management libraries, like Redux. However, one has to be skilled and disciplined to properly use such tools. I’ve seen cases where Redux has been used improperly, effectively rendering the tool useless.
The Kickass Solution
To drive the point home, mutable state is an undisciplined approach to state management. Yes, mutable state, indeed, is the
goto of state. Just like
goto, it’s an undisciplined approach to managing program flow. Mutable state is the same problem that plagued our programs decades ago — only this time we’re dealing with state instead of code.
Most of us are trying hard to answer the complex question of “how did I get to this state?” instead of the same older question “how did I get to this point of execution?”
We really should be spending our time solving real problems and delivering value instead of trying to answer the unnecessary questions.
What’s the solution?
“I think that large objected-oriented programs struggle with increasing complexity as you build this large object graph of mutable objects. You know, trying to understand and keep in your mind what will happen when you call a method and what will the side effects be.”
— Rich Hickey, creator of Clojure
The solution is the complete opposite of mutable state.
I’m talking about immutable state.
It’s a very simple idea. Functions can only return new values and can never change the values that were passed in.
Programming with immutable values nowadays is becoming more and more popular. Even modern UI libraries like
React are intended to be used with immutable values. Languages with first-class support for immutable data values will be ranked higher simply because immutability eliminates a whole category of bugs from our code.
OK, but what’s immutable state exactly? Simply put, it’s data that doesn’t change — just like strings in most programming languages. For example, capitalizing a string will never change the original string. A new string will always be returned instead.
Nothing changed, nothing shared. Ever.
Immutability takes this idea further and makes sure that nothing is ever changed. A new array will always be returned instead of changing the original one. Updating the user’s name? A new user object will be returned with its name updated, leaving the original one intact.
With immutable state, nothing is shared; therefore, we no longer have to worry about the complexity of thread safety. Immutability makes our code easy to parallelize.
Functions that don’t mutate (change) any state are called pure and are significantly easier to test and reason about. When working with pure functions, we never have to worry about anything outside of the function. Just focus on this one function you’re working with, and forget about everything else. You can probably imagine how much easier development becomes (in comparison to OOP, where an entire graph of objects has to be kept in mind).
I’ll cover pure functions in more depth later in this article.
Example with immutable state
array.sort would return a new array, instead of sorting the original array in place.
The example from the previous section could’ve easily been rewritten in the following way to avoid mutation:
And, indeed, this approach produces the expected result:
Before: [3, 5, 4, 1, 2, 9]
After: [1, 2, 3, 4, 5, 9]
In fact, you’ve already encountered this idea in other languages. Strings are immutable in most programming languages.
string.toUpperCase() won’t change the original string — it’ll always return a new string. Working with strings would’ve been terrible had they defaulted to mutating the string in place.
Working with numbers is also immutable. The addition function, as in
2+2, can be thought of as
add(a, b). It doesn’t mutate the original numbers — it simply returns a new number that’s the sum of the values that were passed in.
Advantages of immutable state
Immutable state has numerous advantages over mutable state:
- Programs become easier to reason about (there’s no need to worry about the objects changing over time)
- Concurrency: Immutable data is great for sharing information between multiple threads. Concurrent software and immutable state is a match made in heaven.
- Chaining: Operations on immutable data are easy to chain — e.g.,
"a" + "b" + "c"can be thought of as (
"a" + "b") + "c" == "ab" + "c". This is only possible because the
+operator is immutable and is guaranteed to always return a new string without having any side effects — and without changing the original string.
- Predictable: Immutable data can’t be modified, which means that its state is very predictable — always the same
- Comparison: Instead of performing an expensive deep-equality comparison of two data structures, we can simply compare their hashes, which is nearly instantaneous
- Testing: Functions using immutable data structures are extremely easy to test since they typically have no side effects
Disadvantages of immutable state
You may have noticed that with immutable state, we create a brand new copy of some data structure on every single change. Imagine copying over an array of over 100,000 items over and over again — that must be slow, right? Isn’t this wasteful?
Yes, it is.
That’s why functional programming languages make use of a concept called persistent data structures. This simply means that whenever a change is being made, we don’t have to create a deep copy of the entire data structure.
Instead of creating a copy, persistent data structures simply reuse a reference to the older data structure, while adding in the desired changes.
Persistent data structures
Here’s how a persistent binary tree might be implemented:
Whenever making changes to the
xs tree, the original tree is never modified. Instead, a new tree
ys is created. This tree has its own modified root node
d', which links to the
b node of the original
xs tree. The
e nodes are different in the new
ys tree — however, it still reuses the
h node from the original
Unfortunately, mainstream programming languages typically don’t have built-in support for proper persistent data structures. While solutions using libraries like Redux and Immutable.js are acceptable, it’s preferable to have proper state-management features (e.g., immutable data structures) built into the language. For a list of great (and not-so-great) programming languages, read “These Modern Programming Languages Will Make You Suffer.”
What if switching to a more modern language (that fully supports immutable/persistent data structures) is impossible? Then, you may always be looking for libraries that add support for immutable data structures to your language of choice.
The Functional Age Is Coming
If you’d like to go one step further than immutable state and make your code even better, then you may want to get yourself familiar with functional programming.
I’m a strong believer that functional programming is the future because functional languages have learned from the mistakes of their predecessors. And I’m not alone, functional programming is gaining a massive following worldwide.
Functional programming combines the best ideas from computer science to make developers more productive, while enabling us to write robust and scalable software.
What makes functional programming so great?
First, functional programming makes heavy use of immutable state, and functional programming languages have first-class support for immutable data structures (no need to repeat myself here).
“I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.”
— Tony Hoare, the inventor of null references
Nulls are a terrible way to handle the absence of a value. Null references have been called a “billion-dollar mistake” by Tony Hoare, the person who invented them.
Why are null references bad? Null references break type systems. When null is the default value, we can no longer rely on the compiler to check the validity of the code. Any nullable value is a bomb waiting to explode. What if we attempt to use the value that we didn’t think might be null but is, in fact, null? We get a runtime exception.
We have to rely on manual runtime checks to make sure the value we’re dealing with isn’t null. Even in a statically typed language, null references take away many benefits of a type system.
Such runtime checks (sometimes called null guards) in reality are workarounds for bad language design. They litter our code with boilerplate. And worst of all, there are no guarantees we won’t forget to check for null.
Functional programming encourages the use of an
Option pattern, which forces the developer to explicitly check for a value that may not be present. Even better, functional programming languages type-check the lack or presence of a value at compile time. Typically, the
Option pattern is used:
Catching exceptions isn’t a good way to handle errors. Throwing exceptions is fine — but only in exceptional circumstances, like when the program has no way to recover and has to crash. Just like nulls, exceptions break the type system.
When exceptions are used as a primary way of error handling, it’s impossible to know whether a function will return an expected value or blow up. Functions throwing exceptions are also impossible to compose.
Obviously, it’s not OK for an entire application to crash simply because we couldn’t fetch some data. Yet, more often than we’d like to admit, this is what happens.
One option is to manually check for raised exceptions, but this approach is fragile (we may forget to check for an exception) and adds a lot of noise:
Functional programming generally discourages the use of exceptions. Instead, the
Result pattern is used (which allows for possible errors to be type-checked at compile time):
As I’ve already said, we’ve reached the end of Moore’s law: Processors won’t get any faster, period. We live in the era of multi-core CPUs. Any modern application has to take advantage of multiple cores.
Unfortunately, most of the programming languages in use today were designed in the era of single-core computing and simply don’t have the features to effectively run on multiple cores.
Libraries that help with concurrency are an afterthought — they simply add Band-Aids to languages that weren’t initially designed for concurrency. This doesn’t really count as a good developer experience. In a modern language, concurrency support has to be built in, and most functional languages are great for concurrency.
Programming with pure functions
Unlike imperative programming, functional programming encourages programming with pure functions.
What’s a pure function? The idea is very simple — a pure function will always return the same output, given the same input. For example,
2 + 2 will always return
4, which means that the addition operator,
+, is a pure function.
Pure functions aren’t allowed to interact with the outside world (through making API calls or even writing to the console). They’re not even allowed to change state. It’s the exact opposite of the approach taken by OOP, where any method can freely mutate the state of other objects.
One can easily tell pure functions from impure functions — does the function take no arguments or return no value? Then it’s an impure function.
Here’s an example of a few impure functions:
And a couple of pure functions:
Such an approach may seem to be very limiting and can take some time getting used to. It certainly was confusing for me at first!
What are the benefits of pure functions? They’re very easy to test (no need for mocks and stubs). Reasoning about pure functions is easy — unlike in OOP, there’s no need to keep in mind the entire application state. You only need to worry about the current function you’re working on.
Pure functions can be composed easily. Pure functions are great for concurrency since no state is shared between functions. Refactoring pure functions is pure joy (pun intended) — just copy and paste, no need for complex IDE tooling.
Functional programming encourages the use of pure functions — it’s good when more than 90% of the code base consists of pure functions. Some languages take this to an extreme and disallow nonpure functions altogether (not always a great idea).
Thanks for reading, I hope I was able to convey the benefits of programming with immutable state. Immutable state is the single biggest change one can make to avoid most of the bugs, and finally become a 10x developer. And Functional Programming simply builds on the idea of programming with immutable state.
You can get a better understanding of the topic of immutability by reading Object-Oriented Programming is The Biggest Mistake of Computer Science (it’s not only about OOP).
And if you’re not yet ready to take the plunge into Functional Programming, then Rust is a really good imperative language with great support for immutability.