I/O and Monads
- Every program eventually has to talk to the world
What Is IO? (And What Is a Monad?)
- A function that does I/O has
IOin its return typeStringmeans "a string, computed purely"IO Stringmeans "an action that, when run, produces a string"- This pattern's formal name is a monad
- The compiler enforces that IO only happens where you say it does
- Pure functions are guaranteed not to have side effects
- You can test and reason about them without worrying about hidden state
- This is like Python's
async def: an async function must return anAwaitable- You cannot accidentally run an async function from sync code
- Lean's
IOtype is the same idea: the compiler prevents IO from leaking into pure functions
- Separately,
doand←(left arrow) are how you sequence effectful actions- This is the monad part: chaining steps where each depends on the last
- Like
awaitlets you write sequential-looking code asynchronously,←does the same for IO
- You do not need to understand the theory to use monads in Lean
- (DEBT) We will revisit the theory once we are comfortable doing I/O
Hello World
i
-- Every Lean program starts with main
def main : IO Unit :=
IO.println "Hello, world!"
#eval main
i
Hello, world!
mainis the entry point for Lean programs- Like Python's
if __name__ == "__main__":block
- Like Python's
IO Unitis the return type:IOmeans "does input/output",Unitmeans "no useful return value"Unitis like Python'sNone: the side effects are the point
IO.printlnprints a string followed by a newline- Newline plus indentation isn't required, but it helps
#eval mainruns the action during compilation so we can see the output- Lean doesn't have exceptions
- Errors are handled with sum types like
Except, which we'll cover later (DEBT)
- Errors are handled with sum types like
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
dogroups a sequence of IO actions into one- Statements run top to bottom
- Without
do, we can only write one IO action per function body - Each line in a
doblock runs in order and must returnIO Unit- Unless we capture it: more on that shortly
- If your function body is a single IO action, no
dois needed - If you want two or more actions in sequence, wrap them in
do ;can be used as an alternative separator insidedoblocks- E.g.,
IO.println "Hi"; IO.println "there"is equivalent to two lines
- E.g.,
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.
- Forgetting
dois the most common IO mistake - Without
do, Lean tries to use the secondIO.printlnas an argument to the first- That's why the error message mentions "application type mismatch"
- The fix: add
doafter:=
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!
let x := exprbinds a pure (non-IO) value inside adoblock- Like Python's
x = exprinside an async function
- Like Python's
greetis a plain function that returns aString(no I/O)- You can call pure functions freely inside
do- Only the final
IO.printlndoes I/O
- Only the final
- The compiler will reject
let msg ← greet "Lean"becausegreetdoes not return anIO let _ := expris the discard pattern: it binds a value but throws it away- The underscore means "I don't intend to use this"
- It also suppresses the unused-variable warning from the compiler
- Warning:
let _ := someIOActionstores the action without running it — use←for IO
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
- A function can return an
IO Unit: a reusable IO action- Like Python's
async def, calling it gives you something you still have to run
- Like Python's
printTwice "echo!"runs twoIO.printlncalls in sequence- IO functions compose:
maincallsprintTwicetwice
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!
- Use
let x ← ioAction(the left-arrow←) to capture the result of an IO action- Like Python's
x = await some_coroutine() - Type
\leftarrowin the editor
- Like Python's
makeGreetinghas typeIO String: it is an action that produces aStringreturn exprwraps a pure value as an IO result- This is not the same as Python's
return, which exits the function immediately - Lean's
returnstays in thedoblock: it just marks a value as an IO result - Yes, this is confusing…
- This is not the same as Python's
let msg ← makeGreetingruns the action and binds the resultingStringtomsg- Two binding forms to remember:
let x := expris a pure expression, no IOlet x ← actionis an I/O action, captures its result
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, !
doblocks compose naturally: you can chain as many←binds as you need- Each
←runs the action and captures its result for the next step - Here
getLineis called twice: once for the first name, once for the last name- Both captures are combined in a single
IO.println
- Both captures are combined in a single
- Like writing multiple
awaitcalls in oneasyncfunction
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, !
IO.getStdinreturns anIO.FS.Streamconnected to standard inputstdin.getLinereads one line and returns it as anIO String- Both require
←because they are IO actions that produce values String.trimAsciiremoves leading and trailing whitespace, including the trailing newline- Without
.trimAscii,getLinereturns the newline character as part of the string - This would make
s!"Hello, {name}!"print an unwanted line break
- Without
- The output above shows an empty name because
#evalruns inside the Lean compiler process- The compiler claims stdin for its own use, so
stdin.getLinereturns an empty string - This is a limitation of
#eval, not of Lean's IO system - In the next lesson we will compile and run programs as proper executables, where stdin works correctly
- The compiler claims stdin for its own use, so
Summary
doand←let you chain IO actions in sequence- The practical rules are:
- Use
doto sequence actions - Use
let x ← actionto capture an IO result - Use
let x := exprfor pure computation - Use
return exprto produce an IO result from a pure value
- Use
- Lean's
IOis more than just files and sockets - It also manages random number generation and any other effectful computation
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.printlnlooks 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
makeMsgreturnsIO String, notStringlet msg := makeMsgstores 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 Stringbutmsghas typeString - Lean needs
return msgto lift the pure value intoIO
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
announceprints something and returns nothing useful- Its return type should be
IO Unit, notIO 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 5is a pure function call: it returnsInt, notIO Int←is for IO actions;double 5is 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 plainIO.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.printlncall inside thedoblock - The text should be
"Ready to go!" - Both lines are plain
IO.printlncalls 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 msgwithIO.println msg.toUpper String.toUpper(or.toUpper) converts a string to all capitals- The body is a single expression, so no
doneeded
Write: Build a Message
- Use
++to concatenate strings
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 intoIO - When you fix it,
mainshould printMessage: 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]withlet total := ...(plainlet, not←) - Then use
totalinsides!"Sum is {total}"in theIO.printlncall - 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
doto the body ofrepeat3and callIO.println msgthree times - Each call is a separate line in the
doblock - When fixed,
mainshould printagainthree 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
_nameuses_(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
_nametonameso the binding is used - Replace
"Hello, stranger!"withs!"Hello, {name.trimAscii}!"getLinereturns the line with the trailing newline- Use
.trimAsciito remove it before printing