Functional Programmingbeginner10 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.

To see why that matters, here's the kind of bug it prevents. A sign-up desk keeps a list of names in the order people arrived: Zoe, then Ann, then Max. The first to sign up wins a prize. A leaderboard elsewhere in the program wants the same names sorted A→Z, so both parts are handed the same data.

Step through the scene and predict who the desk thinks signed up first after the leaderboard sorts. Then flip the switch to an immutable list and run the same story again.

What made the difference? let board = signups never copies anything: it's a second name for the same data. In F#, an array ([| ... |]) is a row of slots that can be rearranged in place, so a sort done through one name shows up through every other name. A list ([ ... ]) can't be changed at all, so the only way to "sort" it is to build a new, sorted list. Sharing is harmless when nobody can change the data, and a trap when anybody can.

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.

Check yourself

Your function receives an array from its caller and calls Array.sortInPlace on it to find the middle value. What's the risk?

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.

Watch out

let freezes the name, not always the data. let scores = [| 3; 1; 2 |] means scores will always point at that array, but the array's slots can still be changed: scores[0] <- 99 and Array.sortInPlace scores both work, no mutable needed. F# lists, strings and ordinary records can't be changed at all; arrays (and .NET collections like ResizeArray or Dictionary) can. When you share data between parts of a program, prefer a list or a record.

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. Lists work the same way: in the scene above, "Kai" :: signups made one new cell that points at the three old ones, instead of copying them.

Check yourself

bigList holds a million numbers. Roughly how much new memory does let more = 0 :: bigList need?

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