I/O and Monads

What Is IO? (And What Is a Monad?)

Hello World

i
-- Every Lean program starts with main
def main : IO Unit :=
  IO.println "Hello, world!"

#eval main
i
Hello, world!

Doing More Than One Thing

i
-- Use `do` to run IO actions one after another
def main : IO Unit := do
  IO.println "Step 1: start"
  IO.println "Step 2: middle"
  IO.println "Step 3: done"

#eval main
i
Step 1: start
Step 2: middle
Step 3: done

Forgetting do

i
-- Forgetting `do` is the most common IO mistake
def main : IO Unit :=
  IO.println "Starting..."
  IO.println "Done!"

#eval main
i
io/ex_missing_do.lean:4:2: error: Application type mismatch: The argument
  IO.println
has type
  ?m.3 → IO Unit
but is expected to have type
  Void IO.RealWorld
in the application
  IO.println "Starting..." IO.println
io/ex_missing_do.lean:6:0: error: Aborting evaluation since the expression depends on the 'sorry' axiom, which can lead to runtime instability and crashes.

To attempt to evaluate anyway despite the risks, use the '#eval!' command.

Pure Values Inside do

i
-- `let x := expr` binds a pure value inside a do block
def greet (name : String) : String :=
  s!"Hello, {name}!"

def main : IO Unit := do
  let msg := greet "Lean"
  IO.println msg

#eval main
i
Hello, Lean!

Functions That Return IO Actions

i
-- Functions can return IO actions, not just plain values
def printTwice (msg : String) : IO Unit := do
  IO.println msg
  IO.println msg

def main : IO Unit := do
  printTwice "echo!"
  printTwice "again"

#eval main
i
echo!
echo!
again
again

Capturing Results from IO

i
-- Use ← to capture the result of an IO action
def makeGreeting : IO String := do
  IO.println "Building greeting..."
  return "Hello from IO!"

def main : IO Unit := do
  let msg  makeGreeting
  IO.println msg

#eval main
i
Building greeting...
Hello from IO!

Chaining IO Actions

i
-- Chain multiple ← binds in a single do block
def main : IO Unit := do
  IO.println "Enter your first name:"
  let stdin  IO.getStdin
  let first  stdin.getLine
  IO.println "Enter your last name:"
  let last  stdin.getLine
  IO.println s!"Hello, {first.trimAscii} {last.trimAscii}!"

#eval main
i
Enter your first name:
Enter your last name:
Hello,  !

Reading Input

i
-- IO.getStdin gives access to standard input
def main : IO Unit := do
  IO.println "Enter your name:"
  let stdin  IO.getStdin
  let name  stdin.getLine
  IO.println s!"Hello, {name.trimAscii}!"

#eval main
i
Enter your name:
Hello, !

Summary

Exercises

Fix: Missing do

i
-- This should print two lines but has a bug. Fix it.
def main : IO Unit :=
  IO.println "Hello"
  IO.println "World"

#eval main
i
io/ex_bug_no_do.lean:4:2: error: Application type mismatch: The argument
  IO.println
has type
  ?m.3 → IO Unit
but is expected to have type
  Void IO.RealWorld
in the application
  IO.println "Hello" IO.println
io/ex_bug_no_do.lean:6:0: error: Aborting evaluation since the expression depends on the 'sorry' axiom, which can lead to runtime instability and crashes.

To attempt to evaluate anyway despite the risks, use the '#eval!' command.
hint
  • Without do, Lean treats the body as a single expression
  • The second IO.println looks like an argument to the first

Fix: Wrong Left Arrow

i
-- Should print the greeting, but has a bug. Fix it.
def makeMsg : IO String := return "Hello, Lean!"

def main : IO Unit := do
  let msg := makeMsg
  IO.println msg

#eval main
i
io/ex_bug_let_bind.lean:6:2: error(lean.synthInstanceFailed): failed to synthesize instance of type class
  ToString (IO String)

Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.
io/ex_bug_let_bind.lean:8:0: error: Aborting evaluation since the expression depends on the 'sorry' axiom, which can lead to runtime instability and crashes.

To attempt to evaluate anyway despite the risks, use the '#eval!' command.
hint
  • makeMsg returns IO String, not String
  • let msg := makeMsg stores the IO action itself rather than running it

Fix: Missing return

i
-- This function should return a greeting, but has a bug. Fix it.
def getGreeting : IO String := do
  let msg := "Hello!"
  msg

def main : IO Unit := do
  let g  getGreeting
  IO.println g

#eval main
i
io/ex_bug_return.lean:4:2: error: Type mismatch
  msg
has type
  String
but is expected to have type
  IO String
io/ex_bug_return.lean:10:0: error: Aborting evaluation since the expression depends on the 'sorry' axiom, which can lead to runtime instability and crashes.

To attempt to evaluate anyway despite the risks, use the '#eval!' command.
hint
  • The function body is IO String but msg has type String
  • Lean needs return msg to lift the pure value into IO

Fix: Wrong Return Type

i
-- This function should print a message, but its return type is wrong. Fix it.
def announce (item : String) : IO String := do
  IO.println s!"Announcing: {item}"

def main : IO Unit := do
  announce "Lean 4"

#eval main
i
io/ex_bug_return_type.lean:3:2: error: Type mismatch
  IO.println (toString "Announcing: " ++ toString item)
has type
  IO Unit
but is expected to have type
  IO String
io/ex_bug_return_type.lean:6:2: error: Type mismatch
  announce "Lean 4"
has type
  IO String
but is expected to have type
  IO Unit
io/ex_bug_return_type.lean:8:0: error: Aborting evaluation since the expression depends on the 'sorry' axiom, which can lead to runtime instability and crashes.

To attempt to evaluate anyway despite the risks, use the '#eval!' command.
hint
  • announce prints something and returns nothing useful
  • Its return type should be IO Unit, not IO String

Fix: Arrow on a Pure Expression

i
-- Should print the doubled value, but has a bug. Fix it.
def double (n : Int) : Int := n * 2

def main : IO Unit := do
  let result  double 5
  IO.println s!"Result: {result}"

#eval main
i
io/ex_bug_arrow.lean:5:15: error: Type mismatch
  double 5
has type
  Int
but is expected to have type
  IO ?m.7
io/ex_bug_arrow.lean:8:0: error: Aborting evaluation since the expression depends on the 'sorry' axiom, which can lead to runtime instability and crashes.

To attempt to evaluate anyway despite the risks, use the '#eval!' command.
hint
  • double 5 is a pure function call: it returns Int, not IO Int
  • is for IO actions; double 5 is not one

Fix: Silently Discarded IO Action

i
-- Should print both lines, but only prints one. Fix it.
def main : IO Unit := do
  IO.println "Step 1: start"
  let _ := IO.println "Step 2: done"

#eval main
i
Step 1: start
hint
  • let _ := IO.println "Step 2: done" stores the IO action as a value — it never runs
  • The underscore (_) means "I don't intend to use this binding"
  • For an IO action, you want (run it) or plain IO.println (no binding at all)

Write: Two-Line Greeting

i
-- Add a second line so main prints "Hello, Lean!" then "Ready to go!"
def main : IO Unit := do
  IO.println "Hello, Lean!"

#eval main
i
Hello, Lean!
hint
  • Add a second IO.println call inside the do block
  • The text should be "Ready to go!"
  • Both lines are plain IO.println calls with no

Write: Print in Uppercase

i
-- Fix printLoud so it prints the message in uppercase.
-- Hint: String.toUpper converts a String to all caps.
def printLoud (msg : String) : IO Unit :=
  IO.println msg

def main : IO Unit := do
  printLoud "hello"
  printLoud "world"

#eval main
i
hello
world
hint
  • Replace IO.println msg with IO.println msg.toUpper
  • String.toUpper (or .toUpper) converts a string to all capitals
  • The body is a single expression, so no do needed

Write: Build a Message

i
-- Fix buildMsg so it returns "Message: " ++ text (using return).
def buildMsg (text : String) : IO String :=
  return text

def main : IO Unit := do
  let msg  buildMsg "ready"
  IO.println msg

#eval main
i
ready
hint
  • The function should return "Message: " ++ text
  • Use return ("Message: " ++ text) to lift the pure result into IO
  • When you fix it, main should print Message: ready

Write: Use a Pure Function in IO

i
-- Fix main so it computes and prints the sum of the list (should be 15).
def addUp (xs : List Int) : Int := xs.foldl (· + ·) 0

def main : IO Unit := do
  IO.println "Sum is 0"

#eval main
i
Sum is 0
hint
  • Call addUp [1, 2, 3, 4, 5] with let total := ... (plain let, not )
  • Then use total inside s!"Sum is {total}" in the IO.println call
  • The output should be Sum is 15

Write: Repeat Three Times

i
-- Fix repeat3 so it calls IO.println with msg exactly three times.
def repeat3 (msg : String) : IO Unit := do
  IO.println msg

def main : IO Unit := do
  repeat3 "again"

#eval main
i
again
hint
  • Add do to the body of repeat3 and call IO.println msg three times
  • Each call is a separate line in the do block
  • When fixed, main should print again three times

Write: Read and Greet

i
-- Fix main to read a name from stdin and print "Hello, <name>!"
-- instead of always printing "Hello, stranger!"
def main : IO Unit := do
  let stdin  IO.getStdin
  let _name  stdin.getLine
  IO.println "Hello, stranger!"

#eval main
i
Hello, stranger!
hint
  • _name uses _ (underscore prefix) to suppress the unused-variable warning
    • Lean warns you when a variable is bound but never used
    • Prefixing with _ tells Lean "I'm keeping this binding but not using it yet"
  • Change _name to name so the binding is used
  • Replace "Hello, stranger!" with s!"Hello, {name.trimAscii}!"
    • getLine returns the line with the trailing newline
    • Use .trimAscii to remove it before printing