Use this file to discover all available pages before exploring further.
Lean 4’s do-notation provides a clean, imperative-looking syntax for sequencing monadic computations. Under the hood every do block desugars to a sequence of monadic bind (>>=) operations, but the notation reads naturally as a series of statements. do works with any monad — IO, Option, Except, StateT, and more.
A do block is a sequence of statements. Each statement is one of:
A bindlet x ← action — runs action and binds the result to x
A plain letlet x := expr — binds a pure value
A mutable letlet mut x := expr — introduces a mutable local variable
A discarding bindaction — runs action and discards the result
Control flow: if, for, while, return, break, continue
def greet : IO Unit := do let name ← IO.getLine -- bind: read a line from stdin let greeting := "Hello, " ++ name.trim ++ "!" IO.println greeting -- sequence: print and discard result
let in a do block is immutable. let mut introduces a mutable local variable that can be updated with :=:
def sumTo (n : Nat) : IO Nat := do let mut total := 0 for i in List.range (n + 1) do total := total + i return total
Mutable variables only exist inside the do block in which they are declared. Lean still compiles them to purely functional code using monadic state behind the scenes — there is no shared mutable state.
def classify (n : Int) : IO Unit := do if n > 0 then IO.println s!"{n} is positive" else if n < 0 then IO.println s!"{n} is negative" else IO.println "zero"
for x in collection do iterates over any type with a ForIn instance (arrays, lists, ranges, etc.):
def printList (xs : List String) : IO Unit := do for x in xs do IO.println xdef sumArray (arr : Array Nat) : Nat := Id.run do let mut total := 0 for x in arr do total := total + x return total
Id.run runs a do block in the identity monad, yielding a pure value. Use it when you want for/while loops without any IO.
return x exits the current do block immediately with value x
break exits the nearest enclosing for or while loop
continue skips the rest of the current loop iteration
def findFirst (xs : List Nat) (pred : Nat → Bool) : Option Nat := do for x in xs do if pred x then return x return nonedef printEven (xs : List Nat) : IO Unit := do for x in xs do if x % 2 ≠ 0 then continue IO.println s!"{x} is even"def printUpTo10 (xs : List Nat) : IO Unit := do for x in xs do if x > 10 then break IO.println x
The IO monad represents computations that interact with the outside world. Key IO actions:
Printing
Files
Reading Input
-- IO.println : [ToString α] → α → IO Unit-- IO.print : [ToString α] → α → IO Unit (no newline)-- IO.eprint : [ToString α] → α → IO Unit (stderr)def main : IO Unit := do IO.print "Enter your name: " let name ← IO.getLine IO.println s!"Hello, {name.trim}!"-- String interpolation with s!IO.println s!"2 + 2 = {2 + 2}"
-- Read an entire file as a Stringdef readFileContents (path : String) : IO String := do IO.FS.readFile path-- Write a String to a filedef writeContents (path : String) (contents : String) : IO Unit := do IO.FS.writeFile path contents-- Line-by-line readingdef printLines (path : String) : IO Unit := do let contents ← IO.FS.readFile path for line in contents.splitOn "\n" do IO.println line
-- IO.getLine reads a line from stdin (including newline)def promptNumber : IO Nat := do IO.print "Enter a number: " let line ← IO.getLine match line.trim.toNat? with | some n => return n | none => do IO.println "Not a number, defaulting to 0" return 0
dbg_trace prints a message to stderr during elaboration (in tactic proofs) or at runtime (in programs). It is useful for debugging without modifying the program’s result.
-- In a tactic prooftheorem debug_example (p q : Prop) (h : p ∧ q) : p := by dbg_trace "Starting proof..." exact h.1-- At runtime in a do block (use IO.println for IO code)def factorial (n : Nat) : Nat := dbg_trace "computing factorial of {n}" if n = 0 then 1 else n * factorial (n - 1)
At runtime, dbg_trace writes to stderr and always returns Unit. It is available even in pure code and does not affect the type or value of an expression.
def main : IO Unit := do IO.println "=== Number Guessing Game ===" let secret := 42 let mut attempts := 0 let mut found := false while !found do IO.print "Your guess: " let line ← IO.getLine match line.trim.toNat? with | none => IO.println "Please enter a valid number." | some guess => attempts := attempts + 1 if guess < secret then IO.println "Too low!" else if guess > secret then IO.println "Too high!" else do IO.println s!"Correct! You got it in {attempts} attempts." found := true
How do blocks desugar
let x ← m desugars to m >>= fun x => ... (monadic bind).let x := e desugars to let x := e; ... (pure let).return x desugars to pure x.for x in xs do body desugars using ForIn.forIn which iterates and accumulates via a state machine.break and continue are tunneled through BreakT/ContinueT wrapper monads that wrap OptionT, defined in Init.Control.Do.