Types
- A structure is a product type
- Possible values are the cross-product of the values of the first field with the values of the second field, etc.
- Explore other kinds of types
Sum Types
i
-- A sum type that can hold either a String or an Int
inductive StringOrInt where
| str : String → StringOrInt
| num : Int → StringOrInt
deriving Repr
-- Construct a value of each kind
def greeting : StringOrInt := StringOrInt.str "hello"
def answer : StringOrInt := StringOrInt.num 42
#eval greeting
#eval answer
-- A list can hold a mix of both
def mixed : List StringOrInt := [
StringOrInt.str "hello",
StringOrInt.num 42,
StringOrInt.str "world"
]
#eval mixed
-- Use pattern matching to extract the value
def describe (x : StringOrInt) : String :=
match x with
| StringOrInt.str s => s!"string: \"{s}\""
| StringOrInt.num n => s!"number: {n}"
#eval describe greeting
#eval describe answer
i
StringOrInt.str "hello"
StringOrInt.num 42
[StringOrInt.str "hello", StringOrInt.num 42, StringOrInt.str "world"]
"string: \"hello\""
"number: 42"
- An sum type is a choice between alternatives
- The possible values are the sum of the possibilities of each alternative
- Use
inductiveto define one: we'll explain the name later (DEBT)
StringOrIntis a type whose values wrap either aStringor anIntStringOrInt.strconstructs a string-flavored valueStringOrInt.numconstructs a number-flavored value
deriving Repris like Python's__repr__- We'll explain why it's called
derivinglater (DEBT)
- We'll explain why it's called
- Since every element of the list has type
StringOrInt, the list is homogeneous - Use
matchto extract the wrapped value- The compiler checks that you handle both cases
- We've already seen a sum type:
Optionin basics is eithersomeornone
Trees
i
-- A binary tree with strings at the leaves
inductive Tree where
| leaf : String → Tree
| node : Tree → Tree → Tree
deriving Repr
-- Build a tree
def myTree : Tree :=
Tree.node
(Tree.node
(Tree.leaf "alpha")
(Tree.leaf "beta"))
(Tree.leaf "gamma")
#eval myTree
-- Calculate total length of all strings in the tree
def totalLength (t : Tree) : Nat :=
match t with
| Tree.leaf s => s.length
| Tree.node left right => totalLength left + totalLength right
#eval totalLength myTree
i
Tree.node (Tree.node (Tree.leaf "alpha") (Tree.leaf "beta")) (Tree.leaf "gamma")
14
inductivecan define recursive types, not just flat sumsTreeis either aleafcontaining aString…- …or a
nodecontaining twoTrees
- Like defining a Python class where each instance is either a leaf or a branch
- But the compiler enforces that you can't mix the two accidentally
- This is why the keyword is
inductive- The definition refers to itself:
nodetakes twoTreearguments - Like mathematical induction: base cases (
leaf) and inductive steps (node)
- The definition refers to itself:
totalLengthusesmatchon the tree shape- Compiler checks that code handles both
leafandnodecases- Same exhaustiveness guarantee as
matchon lists
- Same exhaustiveness guarantee as
Polymorphism
i
-- A polymorphic binary tree: leaves can be any type α
inductive Tree (α : Type) where
| leaf : α → Tree α
| node : Tree α → Tree α → Tree α
deriving Repr
-- A function that works for trees of any type
def countLeaves (t : Tree α) : Nat :=
match t with
| Tree.leaf _ => 1
| Tree.node left right => countLeaves left + countLeaves right
-- A tree of numbers
def numTree : Tree Nat :=
Tree.node
(Tree.node
(Tree.leaf 1)
(Tree.leaf 2))
(Tree.leaf 3)
#eval numTree
#eval countLeaves numTree
i
Tree.node (Tree.node (Tree.leaf 1) (Tree.leaf 2)) (Tree.leaf 3)
3
- Add a type parameter to make a type work with any element type
α(alpha) is the conventional Greek letter for the first type parameter- Type it as
\ain Lean
- Type it as
- Same
inductiveshape as the string-onlyTreeabove- But now
leafcan hold any type
- But now
numTreeandstrTreeare the same shape, different leaf types- The compiler prevents you from putting a string leaf in
numTree
- The compiler prevents you from putting a string leaf in
countLeavesworks onTree αfor anyα- Uses
_in theleafbranch because it doesn't need the value, just the count
- Uses
Enumerations
i
-- A sum type with no data: just labels
inductive Season where
| spring | summer | fall | winter
deriving BEq, Repr
#eval Season.summer
#eval Season.summer == Season.winter
#eval Season.summer == Season.summer
i
Season.summer
false
true
- An enumeration is a sum type where no constructor carries data
- Like Python's
enum.Enum
- Like Python's
inductive Seasondefines four possible values with no extra payloadderiving BEqgenerates the==operator- Without it, you can't compare
Seasonvalues - We'll explain the name
BEqlater (DEBT)
- Without it, you can't compare
Sum Types with Data
i
-- A sum type where each constructor carries different data
inductive Shape where
| circle (radius : Float)
| rect (width : Float) (height : Float)
deriving Repr
def s1 : Shape := Shape.circle 3.0
def s2 : Shape := Shape.rect 2.0 5.0
#eval s1
#eval s2
i
Shape.circle 3.000000
Shape.rect 2.000000 5.000000
- Constructors in a sum type can carry one or more data fields
Shape.circletakes aFloatfor the radiusShape.recttakes twoFloats for width and height
- Like a tagged union where each tag stores different-shaped data
- Python equivalent: a
@dataclassdiscriminator field plus a union of subclasses
- Python equivalent: a
Pattern Matching on Sum Types
i
-- Pattern matching must handle every constructor
inductive Shape where
| circle (radius : Float)
| rect (width : Float) (height : Float)
def area : Shape → Float
| Shape.circle r => 3.14159 * r * r
| Shape.rect w h => w * h
#eval area (Shape.circle 3.0)
#eval area (Shape.rect 2.0 5.0)
i
28.274310
10.000000
matchon a sum type works exactly likematchon a list- Each constructor gets its own branch
- The compiler checks that every constructor is handled
areais written in definition-by-cases styleShape.circle r => …destructures the constructor and bindsr
- If you forget a case, the compiler rejects the definition entirely
- Unlike Python, where a missing
casesilently falls through or raises at runtime
- Unlike Python, where a missing
deriving Explained
i
-- 'deriving' generates boilerplate for common type classes
inductive Color where
| red | green | blue
deriving BEq, Repr
-- 'deriving BEq' lets us compare values with ==
#eval Color.red == Color.green
#eval Color.red == Color.red
-- 'deriving Repr' makes #eval display values readably
#eval Color.blue
i
false
true
Color.blue
derivingtells the compiler to generate code for common type classes- (DEBT) A bigger topic we'll return to later
deriving Reprgenerates a readable display for#evalderiving BEqgenerates==- Without it, comparing
Colorvalues is a compile error
- Without it, comparing
Option is a Sum Type
i
-- Option is defined in the standard library as:
-- inductive Option (α : Type) where
-- | none : Option α
-- | some (val : α) : Option α
-- So you can pattern match on it like any other sum type
def describeOpt (x : Option Int) : String :=
match x with
| Option.none => "nothing"
| Option.some n => s!"got {n}"
#eval describeOpt (Option.some 42)
#eval describeOpt Option.none
i
"got 42"
"nothing"
Optionisn't magic: it's defined as aninductivetype in the standard libraryOption.noneis a constructor with no dataOption.someis a constructor that carries one value of typeα
- Because it's a regular sum type, you can pattern match on it directly
- No special syntax: the same
matchyou already know
- No special syntax: the same
- Everything we learned about
Optionin basics was sum types all along
List is a Recursive Inductive Type
i
-- List is defined in the standard library as:
-- inductive List (α : Type) where
-- | nil : List α
-- | cons (head : α) (tail : List α) : List α
-- [] is shorthand for List.nil
-- head :: tail is shorthand for List.cons head tail
#check []
#check 1 :: [2, 3]
#check List.cons 1 (List.cons 2 (List.cons 3 List.nil))
i
[] : List ?m.1
[1, 2, 3] : List Nat
[1, 2, 3] : List Nat
Listis also aninductivetype with two constructorsnilfor the empty list (written[])consfor adding an element to the front (writtenhead :: tail)- The name "cons" is short for "construct", and goes back to the 1950s
::is just syntactic sugar forList.cons[]is syntactic sugar forList.nil- This is why
matchon lists uses[]andhead :: tail: they are the two constructors of the underlying inductive type
Computing on Inductive Types
i
-- A tiny expression language: numbers, addition, multiplication
inductive Expr where
| num (n : Int)
| add (left : Expr) (right : Expr)
| mul (left : Expr) (right : Expr)
deriving Repr
-- Evaluate an expression by recursing on its structure
def eval : Expr → Int
| Expr.num n => n
| Expr.add l r => eval l + eval r
| Expr.mul l r => eval l * eval r
def e : Expr := Expr.add (Expr.num 3) (Expr.mul (Expr.num 4) (Expr.num 5))
#eval e
#eval eval e
i
Expr.add (Expr.num 3) (Expr.mul (Expr.num 4) (Expr.num 5))
23
- Create a tiny expression language in four lines
Expr.numwraps a plain integerExpr.addandExpr.muleach contain two sub-expressions
evalis a recursive function that mirrors the type structure- "Define a type, then write functions by cases" is the heart of Lean
Generic Sum Types
i
-- A generic sum type for success or failure
inductive Result (α : Type) (β : Type) where
| ok (val : α)
| err (msg : β)
deriving Repr
def divide (a : Int) (b : Int) : Result Int String :=
if b == 0 then
Result.err "division by zero"
else
Result.ok (a / b)
#eval divide 10 2
#eval divide 10 0
i
Result.ok 5
Result.err "division by zero"
- A sum type can have multiple type parameters
Result α βis generic over both the success type and the error type
- Like Rust's
Resultor Haskell'sEither - Cleaner than Python's convention of returning
(value, error)tuples- The compiler forces the caller to handle both
okanderr
- The compiler forces the caller to handle both
match with Conditions
i
-- Use conditionals inside match branches when constructors aren't enough
def describe (n : Int) : String :=
match n with
| n' =>
if n' > 0 then "positive"
else if n' < 0 then "negative"
else "zero"
#eval describe 5
#eval describe (-3)
#eval describe 0
i
"positive"
"negative"
"zero"
- Sometimes constructors alone aren't enough: you need finer discrimination
matcharms are just expressions, so you can useif/else if/elseinside them- The
n'pattern matches anyInt, then the conditional further refines the result - Pronounced "en prime"
- The
- The compiler still checks that the
matchis exhaustive- A single
n'branch covers allIntvalues, so no catch-all is needed here
- A single
Nested Patterns
i
-- Match on nested types: destructure an Option containing a List
def describeOptList (x : Option (List Int)) : String :=
match x with
| Option.none => "nothing"
| Option.some [] => "empty list"
| Option.some [n] => s!"singleton: {n}"
| Option.some _ => "longer list"
#eval describeOptList Option.none
#eval describeOptList (Option.some [])
#eval describeOptList (Option.some [7])
#eval describeOptList (Option.some [1, 2, 3])
i
"nothing"
"empty list"
"singleton: 7"
"longer list"
- Types combine: you can have an
Optioncontaining aList matchhandles this by nesting patternsOption.some []matches asomewrapping an empty listOption.some [n]destructures the list inside the option
- Like Python's structural pattern matching (
match/case)- But again, the compiler verifies that every combination is covered
Product Types are Inductive
i
-- A structure groups related values under one name
structure Point where
x : Float
y : Float
deriving Repr
-- A product type is really an inductive with a single constructor
inductive Point' where
| mk (x : Float) (y : Float)
deriving Repr
def p1 : Point := { x := 1.0, y := 2.0 }
def p2 : Point' := Point'.mk 1.0 2.0
#eval p1
#eval p2
i
{ x := 1.000000, y := 2.000000 }
Point'.mk 1.000000 2.000000
structureis syntactic sugar forinductivewith exactly one constructorPoint.mk 1.0 2.0and{ x := 1.0, y := 2.0 }are equivalent
- Product types (structures) and sum types are both
inductiveunder the hood- A structure has one constructor; a sum type has two or more
- The type system unifies both concepts
A Command Interpreter
i
-- A tiny command language in three lines of type definition
inductive Cmd where
| forward (steps : Nat)
| turn (degrees : Int)
| say (msg : String)
deriving Repr
-- Pattern matching plus recursion equals an interpreter
def run (cmds : List Cmd) : String :=
match cmds with
| [] => "done"
| Cmd.say msg :: rest => s!"said '{msg}'; {run rest}"
| Cmd.forward n :: rest => s!"moved {n}; {run rest}"
| Cmd.turn d :: rest => s!"turned {d}°; {run rest}"
def program : List Cmd := [
Cmd.forward 3,
Cmd.say "hello",
Cmd.turn 90
]
#eval run program
i
"moved 3; said 'hello'; turned 90°; done"
- Define a tiny domain-specific language in three lines of type definition
Cmd.forward nmoves forwardnstepsCmd.turn dturnsddegreesCmd.say msgprints a message
runis an interpreter: pattern matching plus recursion over the command list- This is the payoff: define your data shapes with
inductive, then process them withmatch
Exercises
Fix: Missing Constructor in Match
i
-- This function should describe a StringOrInt, but the match
-- is missing a case. Fix it.
inductive StringOrInt where
| str : String → StringOrInt
| num : Int → StringOrInt
def describe (x : StringOrInt) : String :=
match x with
| StringOrInt.str s => s!"string: \"{s}\""
hint
- The
describefunction only handlesStringOrInt.strbut notStringOrInt.num - Add a branch for the
numconstructor that formats the integer value
Fix: Missing deriving Clause
i
-- Comparing Color values should work, but the code won't compile.
-- Fix it by adding a `deriving` clause.
inductive Color where
| red | green | blue
def isRed (c : Color) : Bool :=
c == Color.red
hint
Coloris missing aderivingclause- The
==operator needs aBEqinstance to compare values - Add
deriving BEq(andReprwouldn't hurt) to theinductiveline
Fix: Missing Base Case in Recursion
i
-- This function should sum all numbers in a tree, but the match
-- is missing the leaf case. Fix it.
inductive IntTree where
| leaf : Int → IntTree
| node : IntTree → IntTree → IntTree
def totalSum (t : IntTree) : Int :=
match t with
| IntTree.node left right => totalSum left + totalSum right
hint
totalSumhandlesIntTree.nodebut notIntTree.leaf- The compiler rejects incomplete pattern matches on inductive types
- Add a branch for
IntTree.leaf nthat returnsn
Fix: Unhandled Error Case
i
-- This function should describe both success and failure,
-- but the match only handles the ok case. Fix it.
inductive Result (α : Type) (β : Type) where
| ok (val : α)
| err (msg : β)
def describe (r : Result Int String) : String :=
match r with
| Result.ok n => s!"success: {n}"
hint
describeonly matchesResult.okbut notResult.err- The compiler checks that every constructor of a sum type is handled
- Add a branch for
Result.err msgthat formats the error message
Fix: Missing Case in eval
i
-- This eval function should handle all expression types,
-- but the mul case is missing. Fix it.
inductive Expr where
| num (n : Int)
| add (left : Expr) (right : Expr)
| mul (left : Expr) (right : Expr)
def eval : Expr → Int
| Expr.num n => n
| Expr.add l r => eval l + eval r
hint
evalhandlesExpr.numandExpr.addbut notExpr.mul- The compiler reports which constructors are missing
- Add a line for
Expr.mul l rthat callsevalon both sides and multiplies
Fix: Wrong Return Type
i
-- Count the leaves in a polymorphic tree. The return type
-- annotation is wrong. Fix it.
inductive Tree (α : Type) where
| leaf : α → Tree α
| node : Tree α → Tree α → Tree α
def countLeaves (t : Tree α) : String :=
match t with
| Tree.leaf _ => 1
| Tree.node left right => countLeaves left + countLeaves right
hint
- The
countLeavesfunction body returnsNatvalues (1,+onNat) - But the type annotation says
String - Fix the return type annotation to match what the body actually computes
Write: Weekday or Weekend
i
-- Write a function 'isWeekend' that returns true for Saturday and Sunday.
inductive Day where
| monday | tuesday | wednesday | thursday | friday | saturday | sunday
deriving BEq, Repr
def isWeekend (d : Day) : Bool :=
false
#guard isWeekend Day.saturday == true
#guard isWeekend Day.sunday == true
#guard isWeekend Day.monday == false
hint
- Use
matchon theDayargument to check which day it is - Return
trueforDay.saturdayandDay.sunday,falsefor everything else - Use
_as a catch-all for the five weekdays to keep the code short
Write: Tree Height
i
-- Write a function 'height' that computes the height of a Tree.
-- The height of a leaf is 0; a node's height is 1 + max of children.
inductive Tree (α : Type) where
| leaf : α → Tree α
| node : Tree α → Tree α → Tree α
deriving Repr
def height (t : Tree α) : Nat :=
0
#guard height (Tree.leaf "x") == 0
#guard height (Tree.node (Tree.leaf "a") (Tree.leaf "b")) == 1
#guard height (Tree.node (Tree.node (Tree.leaf "a") (Tree.leaf "b")) (Tree.leaf "c")) == 2
hint
- Use
match t withto handle the two constructors:Tree.leafandTree.node - A leaf has height
0; a node's height is1plus the larger of its children - Use
Nat.maxto compare twoNatvalues
Write: Map from Scratch
i
-- Write 'myMap' that works like List.map, but using the inductive
-- List constructors directly (List.nil and List.cons) instead of
-- the [] and :: shorthand. This reveals what's really happening.
def myMap (f : α → β) (xs : List α) : List β :=
[]
#guard myMap (fun x => x + 1) [] == []
#guard myMap (fun x => x + 1) [1, 2, 3] == [2, 3, 4]
hint
- Use
matchon the list with the inductive constructors:List.nilandList.cons List.nilis the empty list;List.cons h tputs elementhat the front of listt- In the
conscase, applyfto the head and recurse on the tail
Write: Safe Division
i
-- Write 'safeDiv' that returns an Option Int:
-- Option.some of the quotient if b ≠ 0, Option.none otherwise.
def safeDiv (a : Int) (b : Int) : Option Int :=
Option.none
#guard safeDiv 10 2 == Option.some 5
#guard safeDiv 10 0 == Option.none
#guard safeDiv 0 5 == Option.some 0
hint
- Check if
bis zero using anif/elseexpression - Return
Option.nonewhenb == 0,Option.some (a / b)otherwise - This is like Python's
Nonebut the compiler forces callers to handle it
Write: Mirror a Tree
i
-- Write 'mirrorTree' that swaps left and right subtrees of every node.
-- A leaf is unchanged.
inductive Tree (α : Type) where
| leaf : α → Tree α
| node : Tree α → Tree α → Tree α
deriving BEq, Repr
def mirrorTree (t : Tree α) : Tree α :=
t
#guard mirrorTree (Tree.leaf 1) == Tree.leaf 1
#guard mirrorTree (Tree.node (Tree.leaf "a") (Tree.leaf "b")) == Tree.node (Tree.leaf "b") (Tree.leaf "a")
hint
- Use
match t withto distinguishTree.leaffromTree.node - A leaf is unchanged: return it as-is
- For a node, create a new node with left and right subtrees swapped using recursion
Write: Get or Else
i
-- Write 'getOrElse' that returns the value inside an Option if it's 'some',
-- or a default value if it's 'none'.
def getOrElse (opt : Option α) (default : α) : α :=
default
#guard getOrElse (Option.some 5) 0 == 5
#guard getOrElse (Option.none : Option Int) 0 == 0
#guard getOrElse (Option.some "hello") "world" == "hello"
hint
- Use
match opt withto handleOption.someandOption.none Option.some valmeans returnval(ignore the default)Option.nonemeans return thedefaultargument