Mutable State
Lean provides controlled mutation through StateM and local mutability with let mut—no global mutable state.
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 : Nat67def addScore (points : Nat) : StateM GameState Unit :=8 modify fun s => { s with score := s.score + points }910def loseLife : StateM GameState Bool := do11 modify fun s => { s with lives := s.lives - 1 }12 let s ← get13 return s.lives > 01415def nextLevel : StateM GameState Unit :=16 modify fun s => { s with level := s.level + 1 }1718def gameRound : StateM GameState String := do19 addScore 10020 let alive ← loseLife21 if alive then22 nextLevel23 return "Continue"24 else25 return "Game Over"2627#eval gameRound.run ⟨0, 3, 1⟩ -- ("Continue", { score := 100, lives := 2, level := 2 })IO with State
Use ST for mutation within 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.getLine6 let n := input.trim.toNat!7 count := count + n8 IO.println s!"Final count: {count}"Deep Dive: References and ST
For heap-allocated mutable references, Lean provides ST.Ref:
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 -- 20References are useful when you need to share mutable state across different parts of your code.
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