Mutable State
Pure Lean code has no mutable variables. State is either threaded through functions with StateM or kept local to a do block with let mut.
Local Mutability with let mut
Inside do blocks, you can use let mutfor local mutable variables:
1def sum (xs : List Nat) : Nat := Id.run do2 let mut total := 03 for x in xs do4 total := total + x5 return total67#eval sum [1, 2, 3, 4, 5] -- 15Id.run runs a do block that uses mutability but has no other effects. The result is a pure value.Common Patterns with let mut
Accumulators
A common use of mutable state is accumulating a result. Instead of threading an accumulator through recursive calls, you can update a mutable variable directly.
1def factorial (n : Nat) : Nat := Id.run do2 let mut result := 13 for i in [1:n+1] do4 result := result * i5 return result67#eval factorial 5 -- 120Building Collections
You can build up a collection by starting with an empty list and appending elements as you go. This is especially useful in for loops.
1def evens (xs : List Nat) : List Nat := Id.run do2 let mut result : List Nat := []3 for x in xs do4 if x % 2 == 0 then5 result := result ++ [x]6 return result78#eval evens [1, 2, 3, 4, 5, 6] -- [2, 4, 6]Avoid Quadratic Appends
Repeatedly appending to the end of a list can be slow. Build with ::and reverse at the end when possible.
1def evensFast (xs : List Nat) : List Nat := Id.run do2 let mut acc : List Nat := []3 for x in xs do4 if x % 2 == 0 then5 acc := x :: acc6 return acc.reverseCounting
Counting elements that satisfy a predicate is straightforward with a mutable counter. Increment whenever the condition holds.
1def countWhere (p : α → Bool) (xs : List α) : Nat := Id.run do2 let mut count := 03 for x in xs do4 if p x then5 count := count + 16 return count78#eval countWhere (· > 3) [1, 5, 2, 7, 3, 8] -- 3The StateM Monad
For more structured state, use StateM:
1-- StateM σ α: computation with state of type σ, returning α2-- get: read the state3-- set: write the state4-- modify: transform the state56def counter : StateM Nat Nat := do7 let n ← get -- Read current state8 set (n + 1) -- Update state9 return n -- Return old value1011-- Run with initial state12#eval counter.run 0 -- (0, 1) -- returned 0, new state is 113#eval counter.run 10 -- (10, 11)Multiple State Operations
1def countUp : StateM Nat (List Nat) := do2 let mut results := []3 for _ in [0:5] do4 let n ← get5 results := results ++ [n]6 modify (· + 1)7 return results89#eval countUp.run 0 -- ([0, 1, 2, 3, 4], 5)StateM is explicit about state: you can see from the type that a function uses mutable state, unlike global variables in other languages.State Operations
StateM provides a small set of core operations for reading and writing state. Combine them to build more complex stateful computations.
1-- get: read state2def readState : StateM String String := get34-- set: replace state5def writeState : StateM String Unit := set "new value"67-- modify: transform state8def appendState (s : String) : StateM String Unit :=9 modify (· ++ s)1011-- modifyGet: modify and return value12def pop : StateM (List α) (Option α) :=13 modifyGet fun14 | [] => (none, [])15 | x :: xs => (some x, xs)Complex State Types
When you need to track several related values, define a structure for your state. The with syntax makes updates ergonomic.
1-- Use a structure for complex state2structure GameState where3 score : Nat4 lives : Int5 level : Nat6 deriving Repr78def addScore (points : Nat) : StateM GameState Unit :=9 modify fun s => { s with score := s.score + points }1011def loseLife : StateM GameState Bool := do12 modify fun s => { s with lives := s.lives - 1 }13 let s ← get14 return s.lives > 01516def nextLevel : StateM GameState Unit :=17 modify fun s => { s with level := s.level + 1 }1819def gameRound : StateM GameState String := do20 addScore 10021 let alive ← loseLife22 if alive then23 nextLevel24 return "Continue"25 else26 return "Game Over"2728#eval gameRound.run ⟨0, 3, 1⟩ -- ("Continue", { score := 100, lives := 2, level := 2 })let mut Inside IO
let mut is not tied to Id.run — it works in any do block, including IO:
1def interactiveCounter : IO Unit := do2 let mut count := 03 for _ in [0:3] do4 IO.print s!"Count is {count}. Add how much? "5 let input ← (← IO.getStdin).getLine6 let n := input.trimAscii.toString.toNat!7 count := count + n8 IO.println s!"Final count: {count}"let mut is not real mutation. The compiler rewrites the loop into a fold that threads the value through — which is why the variable cannot escape its do block, and why you cannot capture it in a closure and expect updates to be visible. When you genuinely need shared, aliasable state, you want a reference.References: IO.Ref and ST.Ref
A reference is a real mutable cell on the heap. Unlike let mut, it can be passed to other functions, stored in a structure, and shared.
1def useRef : IO Nat := do2 let ref ← IO.mkRef 0 -- Create a reference3 ref.modify (· + 10) -- Modify it4 ref.modify (· * 2) -- Modify again5 ref.get -- Read final value67#eval useRef -- 2089-- The point of a Ref: another function can update YOUR cell10def bump (r : IO.Ref Nat) : IO Unit := r.modify (· + 1)1112#eval show IO Nat from do13 let r ← IO.mkRef 014 bump r15 bump r16 bump r17 r.get -- 3IO.Ref is ST.Ref specialised to IO. The ST monad is the interesting one: it gives you real mutation, and then runST hands you back a pure value, because the type system guarantees no reference escaped.
1-- Genuinely mutable inside, genuinely pure outside.2-- The σ is the "region" tag; annotating the ref's type keeps3-- instance resolution un-stuck.4def sumST (xs : List Nat) : Nat := runST fun σ => do5 let acc : ST.Ref σ Nat ← ST.mkRef 06 for x in xs do7 acc.modify (· + x)8 acc.get910#eval sumST [1, 2, 3, 4, 5] -- 151112-- Note the type: no IO, no monad. It is an ordinary Nat.13#check sumST -- sumST (xs : List Nat) : Natfoldl or recursion if it is natural; let mut + Id.run for a loop that reads imperatively; StateM when the state is threaded through many functions; ST/IO.Refonly when you need aliasing, or in-place updates for performance.Purely Functional Alternatives
Often, you can avoid mutation entirely:
1-- Mutable version2def sumMut (xs : List Nat) : Nat := Id.run do3 let mut total := 04 for x in xs do5 total := total + x6 return total78-- Pure version with fold9def sumPure (xs : List Nat) : Nat :=10 xs.foldl (· + ·) 01112-- Pure version with recursion13def sumRec : List Nat → Nat14 | [] => 015 | x :: xs => x + sumRec xs1617-- All produce the same result18#eval sumMut [1, 2, 3, 4, 5] -- 1519#eval sumPure [1, 2, 3, 4, 5] -- 1520#eval sumRec [1, 2, 3, 4, 5] -- 15Build a list of running totals using let mut.
1def runningTotals (xs : List Nat) : List Nat := Id.run do2 let mut acc : List Nat := []3 let mut total := 04 for x in xs do5 total := total + x6 acc := acc ++ [total]7 return acc89#eval runningTotals [1, 2, 3, 4] -- [1, 3, 6, 10]When to Use Each Approach
- let mut + Id.run: Local mutation in otherwise pure code
- let mut in IO: Mutation with I/O effects
- StateM: When state is central to the computation
- IO.mkRef: Shared mutable references
- Pure recursion/fold: When mutation isn't needed