Control Flow and Structures
Most languages let you lie to the compiler. Lean does not. There are no statements, only expressions. Every if produces a value. Every match is exhaustive or the compiler complains. Every recursive function must terminate or you must convince the system otherwise. Where imperative languages let you wave your hands, Lean demands you show your work.
Conditionals
Lean’s if-then-else is an expression, not a statement. Every branch must produce a value of the same type, and the entire expression evaluates to that value. There is no void, no implicit fall-through, no forgetting to return. The ternary operator that other languages treat as a curiosity is simply how conditionals work here.
-- If-then-else expressions
def absolute (x : Int) : Int :=
if x < 0 then -x else x
#eval absolute (-5) -- 5
#eval absolute 3 -- 3
-- Nested conditionals
def classifyNumber (n : Int) : String :=
if n < 0 then "negative"
else if n == 0 then "zero"
else "positive"
#eval classifyNumber (-10) -- "negative"
#eval classifyNumber 0 -- "zero"
#eval classifyNumber 42 -- "positive"
-- Conditionals are expressions, not statements
def minValue (a b : Nat) : Nat :=
if a < b then a else b
#eval minValue 3 7 -- 3
Pattern Matching
Pattern matching with match expressions lets you destructure data and handle cases exhaustively. The compiler verifies you have covered every possibility, which eliminates an entire category of bugs that haunt languages where the default case is “do nothing and hope for the best.” You can match on constructors, literals, and multiple values simultaneously.
-- Pattern matching with match
def describeList {α : Type} (xs : List α) : String :=
match xs with
| [] => "empty"
| [_] => "singleton"
| [_, _] => "pair"
| _ => "many elements"
#eval describeList ([] : List Nat) -- "empty"
#eval describeList [1] -- "singleton"
#eval describeList [1, 2] -- "pair"
#eval describeList [1, 2, 3, 4] -- "many elements"
-- Matching on multiple values
def fizzbuzz (n : Nat) : String :=
match n % 3, n % 5 with
| 0, 0 => "FizzBuzz"
| 0, _ => "Fizz"
| _, 0 => "Buzz"
| _, _ => toString n
#eval (List.range 16).map fizzbuzz
-- ["FizzBuzz", "1", "2", "Fizz", "4", "Buzz", ...]
-- Guards in pattern matching
def classifyAge (age : Nat) : String :=
match age with
| 0 => "infant"
| n => if n < 13 then "child"
else if n < 20 then "teenager"
else "adult"
#eval classifyAge 5 -- "child"
#eval classifyAge 15 -- "teenager"
#eval classifyAge 30 -- "adult"
Simple Recursion
Recursive functions are fundamental to functional programming. A function processing a list works through elements one by one, patient and systematic, eventually reaching the empty case and returning upstream with its catch. In Lean, functions that call themselves must be shown to terminate. For simple structural recursion on inductive types like Nat or List, Lean can automatically verify termination.
-- Simple recursion on natural numbers
def factorial : Nat → Nat
| 0 => 1
| n + 1 => (n + 1) * factorial n
#eval factorial 5 -- 120
#eval factorial 10 -- 3628800
-- Recursion on lists
def sum : List Nat → Nat
| [] => 0
| x :: xs => x + sum xs
#eval sum [1, 2, 3, 4, 5] -- 15
-- Computing the length of a list
def length {α : Type} : List α → Nat
| [] => 0
| _ :: xs => 1 + length xs
#eval length [1, 2, 3] -- 3
Tail Recursion
Naive recursion can overflow the stack for large inputs because each recursive call adds a frame. Tail recursion solves this by restructuring the computation so the recursive call is the last operation, allowing the compiler to optimize it into a loop. Scheme mandated tail call optimization in 1975. Most other languages did not, which is why stack traces exist.
-- Naive recursion can overflow the stack for large inputs
-- Tail recursion uses an accumulator to avoid this
-- Tail-recursive factorial
def factorialTR (n : Nat) : Nat :=
let rec go (acc : Nat) : Nat → Nat
| 0 => acc
| k + 1 => go (acc * (k + 1)) k
go 1 n
#eval factorialTR 20 -- 2432902008176640000
-- Tail-recursive sum
def sumTR (xs : List Nat) : Nat :=
let rec go (acc : Nat) : List Nat → Nat
| [] => acc
| x :: rest => go (acc + x) rest
go 0 xs
#eval sumTR (List.range 1000) -- Sum of 0..999
-- Tail-recursive reverse
def reverseTR {α : Type} (xs : List α) : List α :=
let rec go (acc : List α) : List α → List α
| [] => acc
| x :: rest => go (x :: acc) rest
go [] xs
#eval reverseTR [1, 2, 3, 4, 5] -- [5, 4, 3, 2, 1]
Structural Recursion
Lean requires all recursive functions to terminate, which prevents you from accidentally writing infinite loops and passing them off as proofs. For simple cases where the recursive argument structurally decreases, Lean verifies termination automatically. For more complex cases, you must provide termination hints, essentially explaining to the compiler why your clever recursion scheme actually finishes. The termination checker is not easily fooled.
-- Lean requires recursion to be well-founded
-- Structural recursion on decreasing arguments is automatic
-- Merge sort: structurally recursive
def merge : List Nat → List Nat → List Nat
| [], ys => ys
| xs, [] => xs
| x :: xs, y :: ys =>
if x ≤ y then x :: merge xs (y :: ys)
else y :: merge (x :: xs) ys
-- Insertion sort is simpler and doesn't need termination proofs
def insert' (x : Nat) : List Nat → List Nat
| [] => [x]
| y :: ys => if x ≤ y then x :: y :: ys else y :: insert' x ys
def insertionSort : List Nat → List Nat
| [] => []
| x :: xs => insert' x (insertionSort xs)
#eval insertionSort [3, 1, 4, 1, 5, 9, 2, 6] -- [1, 1, 2, 3, 4, 5, 6, 9]
Structures
Structures group related data with named fields. If you have used records in ML, structs in Rust, or data classes in Kotlin, the concept is familiar. Unlike C structs, Lean structures come with automatically generated accessor functions, projection notation, and none of the memory layout anxiety.
-- Structures group related data with named fields
structure Point where
x : Float
y : Float
deriving Repr
-- Creating structure instances
def origin : Point := { x := 0.0, y := 0.0 }
def point1 : Point := Point.mk 3.0 4.0
def point2 : Point := ⟨1.0, 2.0⟩
-- Accessing fields
#eval point1.x -- 3.0
#eval point1.y -- 4.0
-- Functions on structures
def distance (p : Point) : Float :=
Float.sqrt (p.x * p.x + p.y * p.y)
#eval distance point1 -- 5.0
Updating Structures
Rather than modifying structures in place, Lean provides the with syntax for creating new structures based on existing ones with some fields changed. This functional update pattern means you never have to wonder whether some other part of the code is holding a reference to your data and will be surprised by your mutations.
-- Updating structures with "with" syntax
def moveRight (p : Point) (dx : Float) : Point :=
{ p with x := p.x + dx }
def moveUp (p : Point) (dy : Float) : Point :=
{ p with y := p.y + dy }
#eval moveRight origin 5.0 -- { x := 5.0, y := 0.0 }
-- Multiple field updates
def translate (p : Point) (dx dy : Float) : Point :=
{ p with x := p.x + dx, y := p.y + dy }
#eval translate origin 3.0 4.0 -- { x := 3.0, y := 4.0 }
Nested Structures
Structures can contain other structures, allowing you to build complex data hierarchies. Lean’s projection notation makes accessing nested fields readable: person.address.city works as you would hope, without the verbose getter chains of enterprise Java.
-- Nested structures
structure Rectangle where
topLeft : Point
bottomRight : Point
deriving Repr
def myRect : Rectangle := {
topLeft := { x := 0.0, y := 10.0 },
bottomRight := { x := 10.0, y := 0.0 }
}
def width (r : Rectangle) : Float :=
r.bottomRight.x - r.topLeft.x
def height (r : Rectangle) : Float :=
r.topLeft.y - r.bottomRight.y
def area (r : Rectangle) : Float :=
width r * height r
#eval area myRect -- 100.0
Default Values
Structures can have default values for their fields, making it easy to create instances with sensible defaults while overriding only specific fields.
-- Structures with default values
structure Config where
host : String := "localhost"
port : Nat := 8080
debug : Bool := false
deriving Repr
-- Use defaults
def defaultConfig : Config := {}
-- Override some defaults
def prodConfig : Config := { host := "api.example.com", port := 443 }
#eval defaultConfig -- { host := "localhost", port := 8080, debug := false }
#eval prodConfig -- { host := "api.example.com", port := 443, debug := false }
Inductive Types: Enumerations
Inductive types let you define custom data types by specifying their constructors. This is the core mechanism that makes Lean’s type system expressive: natural numbers, lists, trees, and abstract syntax all emerge from the same primitive. Simple enumerations have constructors with no arguments; more complex types carry data in each variant.
-- Simple enumerations
inductive Direction where
| north
| south
| east
| west
deriving Repr, DecidableEq
def opposite : Direction → Direction
| .north => .south
| .south => .north
| .east => .west
| .west => .east
#eval opposite Direction.north -- Direction.south
-- Culture ship classes (for the Banksian among us)
inductive CultureShip where
| gsv -- General Systems Vehicle
| gcu -- General Contact Unit
| rou -- Rapid Offensive Unit
| lcu -- Limited Contact Unit
| msu -- Medium Systems Unit
deriving Repr, DecidableEq
def threatLevel : CultureShip → Nat
| .gsv => 1 -- It's a city, not a weapon
| .gcu => 3 -- Diplomacy with teeth
| .rou => 9 -- The teeth
| .lcu => 2 -- Polite but firm
| .msu => 4 -- Versatile
#eval threatLevel CultureShip.rou -- 9
-- Enums with associated data
inductive Shape where
| circle (radius : Float)
| rectangle (width : Float) (height : Float)
| triangle (base : Float) (height : Float)
deriving Repr
def shapeArea : Shape → Float
| .circle r => 3.14159 * r * r
| .rectangle w h => w * h
| .triangle b h => 0.5 * b * h
#eval shapeArea (Shape.circle 2.0) -- ~12.57
#eval shapeArea (Shape.rectangle 3.0 4.0) -- 12.0
#eval shapeArea (Shape.triangle 6.0 4.0) -- 12.0
Recursive Inductive Types
Inductive types can be recursive, allowing you to define trees, linked lists, and other self-referential structures. This is where inductive types earn their name: you define larger values in terms of smaller ones, and the recursion has a base case that grounds the whole edifice.
-- Recursive inductive types
inductive BinaryTree (α : Type) where
| leaf : BinaryTree α
| node : α → BinaryTree α → BinaryTree α → BinaryTree α
deriving Repr
-- Building trees
def exampleTree : BinaryTree Nat :=
.node 1
(.node 2 .leaf .leaf)
(.node 3
(.node 4 .leaf .leaf)
.leaf)
-- Counting nodes
def BinaryTree.size {α : Type} : BinaryTree α → Nat
| .leaf => 0
| .node _ left right => 1 + left.size + right.size
#eval exampleTree.size -- 4
-- Computing depth
def BinaryTree.depth {α : Type} : BinaryTree α → Nat
| .leaf => 0
| .node _ left right => 1 + max left.depth right.depth
#eval exampleTree.depth -- 3
-- In-order traversal
def BinaryTree.inorder {α : Type} : BinaryTree α → List α
| .leaf => []
| .node x left right => left.inorder ++ [x] ++ right.inorder
#eval exampleTree.inorder -- [2, 1, 4, 3]
Parameterized Types
Inductive types can be parameterized, making them generic over the types they contain. This is how you write a List α that works for any element type, or an expression tree parameterized by its literal type. One definition, infinitely many instantiations.
-- Expression trees for a simple language
inductive Expr where
| num : Int → Expr
| add : Expr → Expr → Expr
| mul : Expr → Expr → Expr
| neg : Expr → Expr
deriving Repr
-- Evaluate an expression
def Expr.eval : Expr → Int
| .num n => n
| .add e1 e2 => e1.eval + e2.eval
| .mul e1 e2 => e1.eval * e2.eval
| .neg e => -e.eval
-- (2 + 3) * -4
def expr1 : Expr := .mul (.add (.num 2) (.num 3)) (.neg (.num 4))
#eval expr1.eval -- -20
-- Pretty printing
def Expr.toString : Expr → String
| .num n => s!"{n}"
| .add e1 e2 => s!"({e1.toString} + {e2.toString})"
| .mul e1 e2 => s!"({e1.toString} * {e2.toString})"
| .neg e => s!"(-{e.toString})"
#eval expr1.toString -- "((2 + 3) * (-4))"
Mutual Recursion
Sometimes you need multiple definitions that refer to each other, like even and odd functions that call each other, or a parser and its sub-parsers. Lean supports mutually recursive definitions within a mutual block. The termination checker handles these jointly, so your circular definitions must still demonstrably finish.
-- Mutually recursive definitions
mutual
def isEven : Nat → Bool
| 0 => true
| n + 1 => isOdd n
def isOdd : Nat → Bool
| 0 => false
| n + 1 => isEven n
end
#eval isEven 10 -- true
#eval isOdd 7 -- true
-- Mutually recursive types
mutual
inductive Tree (α : Type) where
| node : α → Forest α → Tree α
inductive Forest (α : Type) where
| nil : Forest α
| cons : Tree α → Forest α → Forest α
end
-- Example forest
def exampleForest : Forest Nat :=
.cons (.node 1 .nil)
(.cons (.node 2 (.cons (.node 3 .nil) .nil))
.nil)
Role Playing Game Example
The constructs above combine naturally in larger programs. What better way to demonstrate this than a D&D character generator? Structures hold character data, inductive types represent races and classes, pattern matching computes racial bonuses, and recursion drives the dice-rolling simulation.
-- D&D Character Generator
-- Run with: lake exe dnd [seed]
structure AbilityScores where
strength : Nat
dexterity : Nat
constitution : Nat
intelligence : Nat
wisdom : Nat
charisma : Nat
deriving Repr
inductive Race where
| human -- The "versatile" option (read: boring but effective)
| elf -- Pointy ears, superiority complex
| dwarf -- Short, angry, likes rocks
| halfling -- Eats seven meals a day, steals your stuff
| dragonborn -- Scales, breath weapon, identity crisis
| tiefling -- Horns and daddy issues
deriving Repr, DecidableEq
inductive CharClass where
| fighter -- Hit things until they stop moving
| wizard -- Glass cannon with a god complex
| rogue -- "I check for traps" (narrator: there were no traps)
| cleric -- The party's reluctant babysitter
| barbarian -- Anger management issues, but make it heroic
| bard -- Seduces the dragon (or tries to)
deriving Repr, DecidableEq
inductive Background where
| soldier -- "In my day, we walked uphill both ways to the dungeon"
| sage -- Actually read the quest briefing
| criminal -- Has a "guy" for everything
| noble -- Insufferable but well-funded
| hermit -- Lived in a cave, has opinions about society
| entertainer -- "Anyway, here's Wonderwall"
deriving Repr, DecidableEq
structure Character where
name : String
race : Race
charClass : CharClass
background : Background
level : Nat
abilities : AbilityScores
hitPoints : Nat
deriving Repr
def racialBonuses (r : Race) : AbilityScores :=
match r with
| .human => { strength := 1, dexterity := 1, constitution := 1,
intelligence := 1, wisdom := 1, charisma := 1 }
| .elf => { strength := 0, dexterity := 2, constitution := 0,
intelligence := 1, wisdom := 0, charisma := 0 }
| .dwarf => { strength := 0, dexterity := 0, constitution := 2,
intelligence := 0, wisdom := 1, charisma := 0 }
| .halfling => { strength := 0, dexterity := 2, constitution := 0,
intelligence := 0, wisdom := 0, charisma := 1 }
| .dragonborn => { strength := 2, dexterity := 0, constitution := 0,
intelligence := 0, wisdom := 0, charisma := 1 }
| .tiefling => { strength := 0, dexterity := 0, constitution := 0,
intelligence := 1, wisdom := 0, charisma := 2 }
def hitDie (c : CharClass) : Nat :=
match c with
| .wizard => 6
| .rogue => 8
| .bard => 8
| .cleric => 8
| .fighter => 10
| .barbarian => 12
structure RNG where
state : Nat
deriving Repr
def RNG.next (rng : RNG) : RNG × Nat :=
let newState := (rng.state * 1103515245 + 12345) % (2^31)
(⟨newState⟩, newState)
def RNG.range (rng : RNG) (lo hi : Nat) : RNG × Nat :=
let (rng', n) := rng.next
let range := hi - lo + 1
(rng', lo + n % range)
def roll4d6DropLowest (rng : RNG) : RNG × Nat :=
let (rng1, d1) := rng.range 1 6
let (rng2, d2) := rng1.range 1 6
let (rng3, d3) := rng2.range 1 6
let (rng4, d4) := rng3.range 1 6
let dice := [d1, d2, d3, d4]
let sorted := dice.toArray.qsort (· < ·) |>.toList
let top3 := sorted.drop 1
(rng4, top3.foldl (· + ·) 0)
def rollAbilityScores (rng : RNG) : RNG × AbilityScores :=
let (rng1, str) := roll4d6DropLowest rng
let (rng2, dex) := roll4d6DropLowest rng1
let (rng3, con) := roll4d6DropLowest rng2
let (rng4, int) := roll4d6DropLowest rng3
let (rng5, wis) := roll4d6DropLowest rng4
let (rng6, cha) := roll4d6DropLowest rng5
(rng6, { strength := str, dexterity := dex, constitution := con,
intelligence := int, wisdom := wis, charisma := cha })
def applyRacialBonuses (base : AbilityScores) (race : Race) : AbilityScores :=
let bonus := racialBonuses race
{ strength := base.strength + bonus.strength
dexterity := base.dexterity + bonus.dexterity
constitution := base.constitution + bonus.constitution
intelligence := base.intelligence + bonus.intelligence
wisdom := base.wisdom + bonus.wisdom
charisma := base.charisma + bonus.charisma }
def abilityModifier (score : Nat) : Int :=
(score : Int) / 2 - 5
def startingHP (con : Nat) (c : CharClass) : Nat :=
let conMod := abilityModifier con
let baseHP := hitDie c
if conMod < 0 && baseHP < conMod.natAbs then 1
else (baseHP : Int) + conMod |>.toNat
def pickRandom {α : Type} (rng : RNG) (xs : List α) (default : α) : RNG × α :=
match xs with
| [] => (rng, default)
| _ =>
let (rng', idx) := rng.range 0 (xs.length - 1)
(rng', xs[idx]?.getD default)
def allRaces : List Race := [.human, .elf, .dwarf, .halfling, .dragonborn, .tiefling]
def allClasses : List CharClass := [.fighter, .wizard, .rogue, .cleric, .barbarian, .bard]
def allBackgrounds : List Background := [.soldier, .sage, .criminal, .noble, .hermit, .entertainer]
def fantasyNames : List String := [
"Thorin Oakenshield", "Elara Nightwhisper", "Grimjaw the Unpleasant",
"Sir Reginald von Pompous III", "Stabitha McBackstab", "Chad Thunderbicep",
"Cornelius the Combustible", "Snarfblatt", "Mysterious Hooded Figure #7",
"That One Guy From That Thing", "Bard McBardface", "Aethon Dragonsbane",
"Pip Goodbarrel", "Zyx'thorax the Unpronounceable"
]
def generateCharacter (seed : Nat) : Character :=
let rng : RNG := ⟨seed⟩
let (rng1, name) := pickRandom rng fantasyNames "Adventurer"
let (rng2, race) := pickRandom rng1 allRaces .human
let (rng3, charClass) := pickRandom rng2 allClasses .fighter
let (rng4, background) := pickRandom rng3 allBackgrounds .soldier
let (_, baseAbilities) := rollAbilityScores rng4
let abilities := applyRacialBonuses baseAbilities race
let hp := startingHP abilities.constitution charClass
{ name := name
race := race
charClass := charClass
background := background
level := 1
abilities := abilities
hitPoints := hp }
def raceName : Race → String
| .human => "Human"
| .elf => "Elf"
| .dwarf => "Dwarf"
| .halfling => "Halfling"
| .dragonborn => "Dragonborn"
| .tiefling => "Tiefling"
def className : CharClass → String
| .fighter => "Fighter"
| .wizard => "Wizard"
| .rogue => "Rogue"
| .cleric => "Cleric"
| .barbarian => "Barbarian"
| .bard => "Bard"
def backgroundName : Background → String
| .soldier => "Soldier"
| .sage => "Sage"
| .criminal => "Criminal"
| .noble => "Noble"
| .hermit => "Hermit"
| .entertainer => "Entertainer"
def formatModifier (score : Nat) : String :=
let m := abilityModifier score
if m >= 0 then s!"+{m}" else s!"{m}"
def printCharacter (c : Character) : IO Unit := do
IO.println "======================================"
IO.println s!" {c.name}"
IO.println "======================================"
IO.println s!" Level {c.level} {raceName c.race} {className c.charClass}"
IO.println s!" Background: {backgroundName c.background}"
IO.println ""
IO.println " ABILITY SCORES"
IO.println " --------------"
IO.println s!" STR: {c.abilities.strength} ({formatModifier c.abilities.strength})"
IO.println s!" DEX: {c.abilities.dexterity} ({formatModifier c.abilities.dexterity})"
IO.println s!" CON: {c.abilities.constitution} ({formatModifier c.abilities.constitution})"
IO.println s!" INT: {c.abilities.intelligence} ({formatModifier c.abilities.intelligence})"
IO.println s!" WIS: {c.abilities.wisdom} ({formatModifier c.abilities.wisdom})"
IO.println s!" CHA: {c.abilities.charisma} ({formatModifier c.abilities.charisma})"
IO.println ""
IO.println s!" Hit Points: {c.hitPoints}"
IO.println s!" Hit Die: d{hitDie c.charClass}"
IO.println "======================================"
def main (args : List String) : IO Unit := do
let seed := match args.head? >>= String.toNat? with
| some n => n
| none => 42
IO.println ""
IO.println " D&D CHARACTER GENERATOR"
IO.println " Type-Safe Adventures Await!"
IO.println ""
let character := generateCharacter seed
printCharacter character
IO.println ""
IO.println "Your adventure begins..."
IO.println "(Use a different seed for a different character: lake exe dnd <seed>)"
Build and run the character generator with:
lake exe dnd 42
Try different seeds to generate different characters. The generator uses a deterministic pseudo-random number generator, so the same seed always produces the same character.