🌓

Mutability Isn’t Variability

How programmers confuse ideas about things changing over time.

Estimated reading time: 7 mins

Bruce Hill October 24, 2024

In programming, there are a lot of benefits to using immutable values, but the benefits are unrelated to whether variables can be reassigned. Immutable values are a type of data that are guaranteed to never change (as opposed to mutable data, which can change). Using immutable values helps you:

In essence, immutability is a guarantee that can make it easier to build complex programs with fewer bugs.

There is a separate, but related concept that I will refer to as “variability”. Variability is something that is not a property of data, but is a property of the symbols we use as a placeholder to represent data. In a programming language, a symbol might be either a constant, like PI, which will hold the same value for the entire lifetime of a program, or a variable like x, which can represent various different values over the course of a program’s lifetime. In functional programming languages, “local constants” are called a “let bindings”, but this term has become hopelessly confusing, because Javascript and Rust both use the keyword let to declare symbols that can take on new values over time. Some languages also have something which I will call a “local constant”, which is a symbol that represents the same value for the duration of the block where the symbol was defined (its lexical scope).

Common Misconceptions

I think that when programmers fail to understand the distinction between mutability and variability, they assume that when a program makes liberal use of constants, it will somehow gain the advantages of immutability. In my opinion, immutable datastructures are dramatically more useful than symbols that can’t be reassigned. Programming languages that properly support immutable datastructures have a powerful toolkit to write bug-free code that lets you approach problems in a fundamentally simpler and safer way than you can without immutable datastructures. On the other hand, languages that support constants (local or global) are only providing a very minor convenience. Although it’s true that you can get very strange behavior by changing the value of Pi, accidental assignment to variables is not a major source of bugs in the real world. Mistaken assignments are the sort of bug that is extremely easy to debug and fix compared to broken invariants and concurrency issues that immutability can help with. Assignments to a specific variable can be easily detected with a debugger or a simple text search. Broken invariants and concurrency issues may take weeks to identify, or even years to detect!

Python as a Case Study

Python is one of many languages that does not have any notion of a “constant” and only has reassignable variables. However, Python treats all strings as immutable and has immutable equivalents of most datastructures (tuples, frozensets, namedtuples, and so on). In Python, assigning a new value to math.pi is not forbidden by the language, but it’s also not a mistake that any programmer would actually make. It’s even less likely to occur when programmers adhere to the convention that ALL_CAPS variables are intended to represent constants. By contrast, using a tuple (immutable) instead of a list (mutable) gives you new abilities, like letting you use a sequence of values as a key in a dictionary because it maintains the invariants of the hash table implementation. Immutability is a superpower in Python, while constants are not even a feature deemed necessary to include in the language.

Constant Folding

I should note that constants are useful for constant folding optimizations in statically compiled languages. If your code has x * 2*PI, you can save a multiplication operation by replacing x * 2*PI with x * 6.28..., but only if you know that it’s impossible for PI to have any value other than 3.14.... For private symbols that are not visible outside of a file, it’s possible to apply this optimization using only static analysis without the need for explicitly declaring constants (any symbol that’s never reassigned can be treated as a constant). For exported symbols, it’s harder to prove they’re never reassigned, so an optimizer may have to forgo some constant folding optimizations if the language doesn’t allow users to explicitly declare constants.

Perpetuating Confusion

I hope that this post has been easy to follow up till now. The concepts don’t have a deep level of complexity, but I think that a lot of confusion arises from how programming languages handle mutability and variability.

C’s const

C is one of the early languages that mixes up these concepts. In C, the keyword const is used both for symbols which cannot be reassigned (constants) and for read-only pointers to datastructures which cannot be mutated (immutable datastructures). C at least allows you to express the ideas separately depending on where the const keyword is placed, but the rules for const annotations are deeply unintuitive. Could you guess the difference between const Foo* foo, Foo const *foo, and Foo* const foo? I’ve done a lot of C programming, and I still find this syntax extremely confusing! Answer: the first two define a variable pointer to an immutable Foo and the last defines a constant pointer to a mutable foo.

Rust’s let mut

Rust is a newer language that should have learned from the past and done a better job, but actually does an especially bad job of handling mutability and variability. In Rust, the let keyword is used to declare local constants (drawing on the history of functional programming languages using “let bindings” for local constants). However, Rust inexplicably uses let mut to declare a local variable that can be reassigned, even when the variable will only hold immutable values. The distinction between mutable and immutable datastructures is hopelessly mixed up with the distinction between constant and variable symbols. You cannot separately express the ideas “this variable can/can’t be reassigned” and “the datastructure this variable references can/can’t be mutated.” The code let mut x = 5; is valid Rust code, but it is neither a “let binding” in the formal sense, nor is the integer referred to by x mutable!

Javascript’s const, var, and let

Javascript does a lot of things wrong, but I think that one thing it does right is to use the syntax var x to declare a variable that can hold different values and const x to declare a constant that holds the same value for its lexical scope. It would be even better if Javascript hadn’t used let x for declaring a variable instead of declaring a “let binding” (a local constant). With languages misusing terminology like this, it’s no wonder people mix these concepts up!

Summing Up

Hopefully this tour of misconceptions and confusions hasn’t obscured the simple core idea: variability refers to whether a symbol like x is constant or variable (reassignable), whereas mutability refers to whether a datastructure can have its contents modified. When programmers (or programming languages) flatten these distinct concepts into a single idea or keyword, it makes it harder to talk about the subject or even to write programs that use these concepts. Immutable datastructures are an essential and powerful tool for building safe programs that rely on invariants. On the other hand, constants can help with readability and occasionally with eking out a bit of extra performance, but they’re unlikely to fundamentally change how you structure a program or think about a problem. Learning the difference between these two concepts will help you as a programmer and should be a core design pillar for programming languages.