Dive C1.02.1
let bindings & immutability
A name binds a value, and the binding holds — reassigning it does not compile.
The most-used keyword in F# is let. It binds a name to a value, and
that binding is immutable by default: the name cannot be pointed at a different value later.
This is the opposite of the variable in most imperative languages, where assignment rebinds in place. The
payoff is that a name's meaning is fixed for the whole scope, so reading code is reading the values once. This
dive covers the binding form, local and nested bindings under the offside rule, and the exact compiler error
that greets an attempt to reassign.
Binding a name to a value
A let binding has one shape: let name = value. The right
side is evaluated once, and the name stands for the result from that point on. F# infers the type, so the
type annotation is optional; the comment after each line below shows what the F# Interactive prints back.
Bindings compose. A later binding can be defined in terms of an earlier one, and the value carries the
inferred type with it — here float, because the literals are floats.
let pi = 3.14159 let r = 2.0 let area = pi * r * r // val area : float = 12.56636
Each name on the left is now welded to its value. pi
is 3.14159 for the rest of the scope; area is the product
computed once. Nothing reassigns later, because in F# a plain let binding cannot be
reassigned at all.
- let name = e
- evaluate
eonce and bind its value toname. - immutable
- the default — the name cannot later be pointed at a different value.
- type inference
- F# works out the type; the annotation is optional.
- val name : T = v
- what F# Interactive echoes — the name, its inferred type, and the value.
Local bindings and the offside rule
A binding can be local to an expression. Indent further-right bindings under a let,
and they exist only inside it; the last expression is the value of the whole block. F# uses indentation to
mark scope — the offside rule — so no braces or in keyword are
needed.
let result = let a = 2 let b = 3 a + b // val result : int = 5
Here a and b are
private to result. Outside the block they are not in scope. Each is an ordinary
immutable binding; the block's last line, a + b, is its result. Local bindings keep
the names a computation needs close to where they are used, without leaking them.
The offside rule means alignment matters. The two inner let lines and
the final expression sit at the same indent, deeper than the outer let result =;
that shared indent is what groups them into one block.
Immutable by default: the FS0027 error
What happens when code does try to reassign a let binding? The compiler rejects it.
F#'s assignment operator is <-, and applying it to a plain binding produces a
specific, named error. Walk the three states below: a binding, the rejected reassignment with the exact
compiler message, and the form that does compile.
A binding, a rejected reassignment, and the fix · select a state
let x = 5 // val x : int = 5 — an immutable binding
The default is not advice; it is enforced. A plain binding cannot be reassigned, and the compiler says so by name — FS0027 — and points at the keyword that would change the default.
the rule
A binding fixes a name to a value. To get a different value you write a different binding.
F# enforces it
x <- 6 on a plain let is rejected at compile time with error FS0027.
Why immutable by default
The default is a deliberate trade. Three properties follow from a name keeping its value.
Equational reasoning. When area is bound once, every later
area means the same thing; a reader can substitute the value and the program holds.
No action at a distance. A function cannot reach back and change a binding a caller relies
on, so a value passed in is the value used. Safe to share. A value that never changes is
safe to read from many threads at once, with no lock and no copy — the concurrency story C3 builds on.
None of this forbids change; it relocates it. When a value genuinely must change in place, F# offers
mutable, covered in C1.02.3. And rebinding a name with a fresh
let — which looks like assignment but is not — is shadowing, the
subject of C1.02.2.
References
Sources
- Microsoft — F# documentation — let bindings, immutability, and the mutable keyword.
- The F# language specification — the binding form, scope, and the rules for mutable values.
- FSharp.Core API reference — the core library against which bindings are typed.
Related in this course
- C1.02.2 — Shadowing — rebinding a name without mutating the old value.
- C1.02 — Bindings — the module hub and its three dives.
- Elixir — immutability — the same default, told for the BEAM.