Explainstuff.mebeta
All concepts
Functional Programmingbeginner6 min

Immutability

Never edit data in place — make a fresh copy with the change, and leave the original perfectly intact.

Picture two ways to make an edit. The first is scribbling over the only paper copy of a document — once you've crossed something out, the original is gone forever, and anyone else reading that page just sees your changes appear out of nowhere. The second is editing a Google Doc that auto-saves every version — your change shows up as a new revision, and the old one is still sitting safely in the history.

Immutability is the second way, applied to data in your programs. An immutable value never changes after it's created. When you want a "changed" version, you don't overwrite the original — you make a new copy with the change baked in, and the original stays exactly as it was.

How it works

The trick is that "changing" immutable data is really creating. You start with an original value, apply a transform, and get back a brand-new value alongside it. The original is never touched — it's still there, unchanged, for anyone who's holding onto it.

This is the opposite of the scribble-over-paper approach, where the change destroys what was there before. With immutable data, the before and after both exist, side by side.

Change makes a copy
with change
original
new copy
The original is never touched — applying a change produces a brand-new value alongside it.

In F#, this is the default way of working. When you write let, you're naming a value that can't be reassigned later:

let x = 5
// x = 6        // not allowed: this isn't assignment, it's a comparison
// x is, and always will be, 5

That name x is bound to 5 for good. There's no sneaky way to make it mean something else later, so you never have to wonder whether some other line of code quietly changed it out from under you.

The real magic shows up with records. Say you have a user and you want to rename them. Instead of editing the record in place, you use the with keyword to make a copy that's identical except for the one field you're changing:

type User = { Name: string; Age: int }

let original = { Name = "Alex"; Age = 30 }
let renamed  = { original with Name = "Sam" }

// original is still { Name = "Alex"; Age = 30 }
// renamed  is now   { Name = "Sam";  Age = 30 }

Notice that original didn't budge. The with expression produced a whole new record, renamed, and left the old one perfectly intact — just like a new revision in your version history.

Note

This is predictable in exactly the way pure functions are. A pure function always gives the same answer for the same input and changes nothing else. Immutable data is the perfect partner: if the values you pass around can't be edited behind your back, then reasoning about what a function does becomes wonderfully boring — and boring is good.

Opting into mutation

F# does let you edit in place when you really want to — you just have to ask for it explicitly with let mutable:

let mutable counter = 0
counter <- counter + 1   // the <- arrow reassigns in place
counter <- counter + 1   // counter is now 2

The <- arrow overwrites the old value, scribble-over-the-paper style. The fact that you have to opt in with mutable is the whole point: editing in place is the exception you reach for deliberately, not the default that bites you by accident.

Why it's great

The biggest win is the disappearance of a whole class of spooky bugs. With mutable data, you can hand the same object to two parts of your program and one of them quietly changes it, leaving the other baffled by the "who changed this behind my back?" mystery. Immutable values simply can't be edited from under you, so that mystery never happens.

That same property makes immutable data safe to share across threads. Because nothing can change a value once it exists, two threads can read it at the same time without any locks or coordination — a big deal for concurrency, and a natural fit for keeping services stateless.

You also get undo and history almost for free. Since every "change" leaves the previous version intact, you can keep the old copies around and step backward through them whenever you like — the same reason a versioned doc lets you restore yesterday's draft.

Tip

Worried that copying everything is wasteful? It usually isn't. Immutable data structures use structural sharing: the new copy reuses all the parts that didn't change and only allocates the small bit that did. Renaming one field in a big record doesn't deep-copy the whole thing — the new value quietly points at the unchanged pieces of the old one.

The new copy reuses what didn't change
points at
renamed
original
Name "Sam"
Name "Alex"
Age 30 (shared)
Renaming allocates only the new Name; both records share the untouched Age. Copying is cheap because unchanged parts are reused, not duplicated.

Put it all together and immutability gives you data you can trust: it won't change when you're not looking, it's safe to share, and you can always get back to where you were. Make new copies instead of editing in place, lean on F#'s defaults, and let your programs become a little more predictable and a lot less spooky.

Key takeaways

  • Immutable data never changes after it's created — to "change" it, you build a new copy with the change applied and leave the original alone.
  • In F#, values are immutable by default: `let x = 5` makes a name you cannot reassign; you need `let mutable` to opt into editing in place.
  • Records get a clean copy-and-update syntax with `with`: `{ user with Name = "Sam" }` returns a brand-new record and leaves the old one untouched.
  • Because nobody can edit shared data from under you, immutable values are safe to pass across threads without locks, and they make undo, history, and debugging easy.
  • Copying isn't as expensive as it sounds — structural sharing lets the new copy reuse all the parts that didn't change.

Keep going