Mutability Isn’t Variability
How programmers confuse ideas about things changing over time.
Estimated reading time: 7 mins
October 24, 2024In 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:
- Safely store values in a cache so the same value can be reused later.
- Use trees or lists with shared subcomponents.
- Preserve invariants in datastructures like hash tables.
- Store unchanging historical records like undo logs.
- Share data between threads safely without synchronization primitives.
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.