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:
lean
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] -- 15ℹ
Id.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
lean
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
lean
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]Counting
lean
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:
lean
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
lean
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)Key Takeaway
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
lean
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
lean
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:
lean
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:
lean
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:
lean
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] -- 15💡
Use mutable state when it's clearer (imperative algorithms) or more efficient. Use pure functions when they're simpler or when you need to reason about the code.
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