Immutability & Pure Functions
rust1-2 made immutability the default, one keyword (mut) away from mutability whenever it's genuinely needed. Haskell doesn't offer that escape hatch in the first place — this chapter explains what that actually buys, precisely.
No Reassignment, Ever
x = 5 is not an assignment — it's a binding, a permanent name-to-value association. There is no mut equivalent anywhere in the pure part of the language. Once x is bound, there is genuinely no way to reassign it, ever, anywhere in its scope — a stronger guarantee than rust1-2's own default, which is real immutability, but only until mut is written.
"Variables" Aren't Variable
The word "variable" is a slight misnomer inherited from mathematics, not from imperative programming. In Haskell, x behaves exactly like a variable in an algebraic equation — a fixed name standing for a fixed value within its scope — not a storage location that can hold different things over time, the way c1-2's and java1-2's own variables genuinely can.
Referential Transparency
An expression is referentially transparent if it can be replaced by its value with zero change to the program's behavior — anywhere, at any time. Since x is never reassigned, x + x can always be swapped for 10. This is a genuine, checkable algebraic property no mutable-variable language on this site can offer: in C or Java, x + x might legitimately produce a different result on a second evaluation if something reassigned x in between.
Why This Matters — Equational Reasoning
A direct, practical consequence: Haskell code can be reasoned about the way algebra is — substitute equals for equals, refactor by simple substitution, and trust that the substitution never changes meaning. This is genuinely different from tracing through an imperative program, where "what is x right now" depends on execution history and ordering, not just the code as written.
"Mutation" via Shadowing and let/where
This can look exactly like reassignment — it isn't. The inner x is a genuinely new, separate binding that happens to reuse the name x, shadowing the outer one. The original binding is untouched and still exists, just no longer reachable by that name in the inner scope. rust1-2 also has shadowing, but Rust's shadowing coexists alongside real mutation via mut — two genuinely distinct mechanisms that can look similar. Haskell only has the one, since it has no true mutation to distinguish shadowing from in the first place.
| Aspect | C/Java | Rust (rust1-2) | Haskell |
|---|---|---|---|
| Default mutability | mutable | immutable | no mutation mechanism exists |
| Opting into mutation | always available | mut keyword | not possible for a binding |
| Shadowing | limited/scoped | real, coexists with mut | the only "reassignment-looking" mechanism there is |
| x + x always substitutable by its value | no | yes, if x is not mut | yes, always |
x = 5 as "define x to mean 5," the same way algebra uses =, avoids most of the early confusion that comes from expecting it to behave like an assignment statement in an imperative language.
let x = ... lines inside a do block can easily read as reassignment on a first pass — it's genuinely a fresh binding each time, not the same x being updated. Worth double-checking scope carefully the first several times this comes up.
Coding Challenges
In GHCi, bind x = 10, then attempt to write a second line rebinding x = 20 at the top level in the same session, and report what actually happens (a genuine rebinding at the top level of GHCi vs. what would happen inside a single expression's scope).
📄 View solutionWrite a small expression using nested let...in bindings that shadows the same name twice, printing the final value, and explain in a comment which binding is actually in scope at each point.
📄 View solutionWrite a short comment explaining why x + x being always substitutable by its value is impossible to guarantee in a language like Java, tying your answer to referential transparency.
📄 View solutionChapter 3 Quick Reference
- x = 5 is a permanent binding, not an assignment — no mut equivalent exists for it anywhere
- Haskell's "variables" behave like mathematical variables — fixed names for fixed values, not storage locations
- Referential transparency: an expression can always be replaced by its value, everywhere, safely — a real, checkable property mutable-variable languages can't offer
- Equational reasoning follows directly — Haskell code can be refactored by substitution, the way algebra is
- Shadowing (let x = ... in let x = ... in ...) looks like reassignment but creates a genuinely new binding — the only such mechanism, since Rust's own shadowing (rust1-2) coexists with real mutation and Haskell's doesn't
- Next chapter: lists and recursion — recursion as the only way to loop, no for/while exist at all