Immutability & Pure Functions

Course 1 · Ch 3
Immutability & Pure Functions
Not opt-in like rust1-2's own mut — there is no reassignment mechanism to opt into at all

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 -- x = 6 -- not "immutable by default" — this simply doesn't mean what it looks like it means

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

x = 5 result = x + x -- can always be replaced by "10" — everywhere, safely, forever

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

let x = 5 in let x = x + 1 in x -- 6 — but this is NOT reassignment

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.

AspectC/JavaRust (rust1-2)Haskell
Default mutabilitymutableimmutableno mutation mechanism exists
Opting into mutationalways availablemut keywordnot possible for a binding
Shadowinglimited/scopedreal, coexists with mutthe only "reassignment-looking" mechanism there is
x + x always substitutable by its valuenoyes, if x is not mutyes, always
Read = as mathematical equality, not an imperative statement
Treating 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.
Shadowing can look exactly like mutation at a glance
Repeated 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

Challenge 1

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 solution
Challenge 2

Write 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 solution
Challenge 3

Write 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 solution

Chapter 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