Module C2.02
From mutable state to immutability & expressions
F# inverts two C# defaults: everything returns a value, and values do not change.
A C# developer reaches for var, mutates a field, and writes a
void method out of statements that run for their effect. F# inverts both habits.
First, everything is an expression: if, match,
and try each return a value, and the last expression in a function is its result —
there is no return keyword. Second, values do not change:
let binds an immutable value, a record has no setters, and a changed version is a new
copy made with { r with … }. Neither shift is a restriction. The first removes
the gap between a statement and the value it computes; the second removes whole classes of bug — aliasing,
and the accidental shared mutation that comes with it. This module covers both in three dives, and ends where F#
does not invert C#: mutation is still available, but contained.
Two inverted defaults, one worked example
The cleanest way to feel the shift is to write the same tiny function in both languages. In C#, a method that
classifies a number runs if as a statement and hands the result back with
return. In F#, the if is the result — the
function is one expression, and its value is the function's value.
C# — statements, return
// a method built from statements static string Grade(int n) { if (n >= 60) return "pass"; return "fail"; }
F# — one expression
// the if IS the result; no return let grade n = if n >= 60 then "pass" else "fail" // val grade : int -> string grade 75 // val it : string = "pass"
The C# method computes a value and then returns it; the F# function is the value. That gap closing is the whole of the first dive.
The module, in three dives
The three dives below move from expressions, to immutability, to the contained mutation F# still allows. Select one to read its summary and the one idea it adds.
The three dives · select one
if, match, and try are expressions that return a value. The last expression is the result; there is no return. A void method becomes a function returning unit.
Open this dive · or pick another above
Everything returns a value; values do not change; and where mutation is needed, it stays inside the function. Those three facts retrain the C# reflexes that fight F#.
The three dives
Read them in order. Each stands on a C# the reader knows beside its idiomatic F#, and on valid, runnable
F#: every val it : echo below was produced by dotnet fsi.
Expressions, not statements
if, match, try return values; the last expression is the result; a void method becomes a function returning unit.
- valueread every construct as a value
Immutable by default
let is not var; records have no setters; a changed copy is { r with … }. Shadowing is not mutation.
- copychange a value by copying it
Mutation, contained
let mutable + <- stays local; ResizeArray and Dictionary are there for hot loops and interop.
- localmutate inside, stay pure outside
References
Sources
- Microsoft — F# documentation — values,
letbindings, records, and the expression model. - Microsoft — C# records and the
withexpression — the C# feature this module bridges from. - The F# Language Specification — expressions, bindings, and mutable values.
Related in this course
- C2.02.1 — Expressions, not statements — the first dive, on the value model.
- C2 — F# for C# developers — the chapter this module sits in.
- C1.02 — let bindings, immutability & shadowing — the same ideas from the language side.
- Elixir — Functional foundations — immutability and expressions on the BEAM.