Part 23 – Immutability
This is a series of posts I’m writing about using types as another tool in software development, Continuous Delivery, & keeping LLM’s honest. They’re also a design & refactoring tool, a communication tool, and reduce how many tests you have to write.
Parts


Mutation is a side-effect, has a long list of bugs it creates, and yet remains the most controversial choice despite using immutable data being easier to do nowadays without the speed tradeoffs. At a distance bug:
user = { permissions: ['read'] }
cachedUser = user
// some code far away from user
// changes it, possibility negatively affecting
// code is knows nothing about
user.permissions.push('admin')Code language: JavaScript (javascript)
Using immutability, everyone has their own copy to prevent this.
In pipelines, you have to read the code to know you call applyDiscount first before you call chargeCustomer. The type signature tells you none of that.
Even in sync code, you can corrupt data if you write imperative code and stop running it halfway:
function submitOrder(order) {
order.status = "submitted"
payment = charge(order)
if (!payment.ok) {
// payment won't have an id
return
}
order.paymentId = payment.id
}Code language: JavaScript (javascript)
Immutability, it only creates new thing when the return is successful, negating partial data updates.
Mutation makes debugging hard:
account = { balance: 100 }
account.balance -= 30
Can’t see previous state, compare it to the current, audit, nor rollback. Immutability has all of that built-in w/ability to build atop.
When you call a function it can change your parameter b/c Array.sort is mutable:
const original = [charlie, alice, bob]
const sorted = sortUsers(original)
// this is also sorted
console.log(original)Code language: JavaScript (javascript)
Pure/Total functions don’t “change the world” before/after they run, preventing this from happening.
Many other bugs like some unit tests breaking other tests randomly, async code where 2 promises can mutate a single value causing problems, breaking the domain business rules by mutating business invariants encoded in types, the list goes on. You have to read all the code to “know the order in which something was mutated”.
Using immutable data, all of your type signatures don’t lie as to what’s happening (with the exclusion of Haskell’s StateMonad or Rust’s borrow checker/exclusion). It does NOT ensure purity/non-determinism, but is part of it. You don’t even need to use readonly, or ReadOnly<T> types; just follow the convention “We don’t mutate things, we just return copies via object spread syntax” (or the Python/Luau/your language equivalent). You don’t even need structuredClone if everyone just follows the convention.
This is the most controversial, even with those not concerned at all by performance, and can be sometimes the non-starter to any good type initiative in engineering cultures. Make no mistake, your life is better & easier without mutation except for various edge cases.
Leave a Reply