100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Values and Immutability

How F#'s let bindings are immutable by default, how to opt into mutability, and how shadowing differs from mutation.

FoundationsBeginner8 min readJul 10, 2026
Analogies

Values and Immutability

In F#, the let keyword binds a name to a value, and by default that binding is immutable — once let x = 5 has executed, x can never be reassigned to point at a different value within that scope. This differs sharply from variables in C#, Python, or JavaScript, where x = 5 typically means 'this variable can change later'; in F#, immutability is the default assumption baked into the language, and mutability must be explicitly requested.

🏏

Cricket analogy: F#'s let x = 5 behaving like a permanent entry on the scorecard rather than a mutable variable is like a run scored being etched into the record the moment the umpire signals it — no one goes back and changes that ball's outcome.

The let Keyword and Binding Semantics

Because let creates a binding rather than a variable slot, let x = 5 is closer to defining a mathematical constant than declaring storage — the compiler enforces this, so x <- 6 on an immutable binding is a compile error, not a runtime surprise. Immutability by default eliminates an entire category of bugs where a shared value is unexpectedly changed by distant code, which is especially valuable in concurrent or multi-threaded systems where mutable shared state is a classic source of race conditions.

🏏

Cricket analogy: F#'s compiler rejecting x <- 6 on an immutable binding is like the third umpire overturning an on-field mistake before it's ever recorded in the official scorebook.

Opting Into Mutability

When mutation truly is the right tool — a loop counter, an accumulator updated in a tight performance loop, a UI-bound value — F# lets you opt in explicitly with let mutable x = 5, after which x <- x + 1 reassigns it using the <- operator, deliberately different from the = used for binding and equality comparison. This explicitness means every mutable variable in an F# codebase is visible at a glance, making it easy to audit exactly where state can change.

🏏

Cricket analogy: Explicitly marking a value mutable in F# is like a team specifically naming a designated pinch runner before the match — everyone knows in advance exactly who's allowed to sub in.

Shadowing vs Mutation

F# also supports shadowing: writing let x = 5 followed later by let x = x + 1 in the same scope does not mutate the original x — it creates a brand-new binding that happens to reuse the name, while the old value still exists (and may still be referenced by a closure created before the shadowing). This is a common source of confusion for beginners coming from mutable-variable languages, who read shadowing as mutation when it is actually the creation of an entirely separate, still-immutable value.

🏏

Cricket analogy: Shadowing let y = y * 2 creating a new binding is like a substitute batter walking in with the same shirt number as the player who just got out — same name on the back, but a completely different player at the crease.

fsharp
let x = 5
// x <- 6   // Compile error: 'x' is not mutable

let mutable counter = 0
counter <- counter + 1
counter <- counter + 1
printfn "Counter: %d" counter   // 2

// Shadowing: creates a new binding, doesn't mutate the old one
let y = 10
let y = y * 2   // new 'y', shadows the old one
printfn "Shadowed y: %d" y      // 20

Immutability by default is one of the biggest reasons F# code tends to have fewer bugs around shared state — if a value can't change, you never have to trace through the whole call stack to find out who modified it.

Don't reach for mutable out of habit from C# or Python. Overusing mutable state defeats F#'s safety guarantees and makes code harder to reason about — reserve it for genuine cases like tight loops or interop with mutable APIs.

  • let in F# creates an immutable binding by default — reassignment is a compile-time error unless the value is marked mutable.
  • Use let mutable x = ... to opt into mutability, and the <- operator, not =, to reassign.
  • Immutability by default reduces bugs from unexpected shared-state changes, especially in concurrent code.
  • Shadowing (let x = ... reused in the same scope) creates a new binding — it does not mutate the original value.
  • A closure created before a shadowing let still sees the old value, since shadowing doesn't touch the original binding.
  • Reserve mutable for genuine cases like loop counters or performance-critical accumulation — default to immutable values.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#FStudyNotes#ValuesAndImmutability#Values#Immutability#Let#Keyword#StudyNotes#SkillVeris#ExamPrep