Pattern Matching
Pattern matching is the primary control flow mechanism in Lean. It's how you deconstruct data and handle different cases elegantly.
The Match Expression
Use match to inspect values and branch based on their structure:
1def describe (n : Nat) : String :=2 match n with3 | 0 => "zero"4 | 1 => "one"5 | 2 => "two"6 | _ => "many" -- underscore matches anything78#eval describe 0 -- "zero"9#eval describe 5 -- "many"Each branch has a pattern (left of =>) and a result (right of =>). Lean checks patterns from top to bottom.
Function Definition Syntax
For simple functions, you can pattern match directly in the definition:
1-- Using match2def factorial1 (n : Nat) : Nat :=3 match n with4 | 0 => 15 | n + 1 => (n + 1) * factorial1 n67-- Direct pattern matching (cleaner)8def factorial2 : Nat → Nat9 | 0 => 110 | n + 1 => (n + 1) * factorial2 n1112#eval factorial2 5 -- 120match) is often cleaner for simple functions. Use match when you need to match in the middle of a larger expression.Matching on Inductive Types
Pattern matching shines when working with inductive types:
1inductive Shape where2 | circle (radius : Float)3 | rectangle (width : Float) (height : Float)4 | triangle (base : Float) (height : Float)56def area : Shape → Float7 | Shape.circle r => 3.14159 * r * r8 | Shape.rectangle w h => w * h9 | Shape.triangle b h => 0.5 * b * h1011def perimeter : Shape → Float12 | Shape.circle r => 2.0 * 3.14159 * r13 | Shape.rectangle w h => 2.0 * (w + h)14 | Shape.triangle _ _ => 0.0 -- would need side lengths1516#eval area (Shape.rectangle 3.0 4.0) -- 12.0Exhaustiveness Checking
Lean refuses to compile if you miss a case:
1inductive Color where2 | red | green | blue34-- This won't compile!5-- def toHex : Color → String6-- | Color.red => "#FF0000"7-- | Color.green => "#00FF00"8-- -- Missing blue case!910-- Complete version:11def toHex : Color → String12 | Color.red => "#FF0000"13 | Color.green => "#00FF00"14 | Color.blue => "#0000FF"Destructuring Structures
Pattern matching works on structures too:
1structure Point where2 x : Float3 y : Float45-- Destructure in the pattern6def distanceFromOrigin : Point → Float7 | ⟨x, y⟩ => Float.sqrt (x * x + y * y)89-- Equivalent using field access10def distanceFromOrigin' (p : Point) : Float :=11 Float.sqrt (p.x * p.x + p.y * p.y)1213#eval distanceFromOrigin ⟨3.0, 4.0⟩ -- 5.0Nested Patterns
Patterns can be nested to match complex structures. This is powerful when you have data types inside other data types, such as a list containing optional values.
1-- Match on nested structures2def firstElement : List (Option Nat) → Option Nat3 | [] => none4 | none :: _ => none5 | some n :: _ => some n67#eval firstElement [some 1, none] -- some 18#eval firstElement [none, some 2] -- none910-- Match on pairs11def addPair : Nat × Nat → Nat12 | (a, b) => a + b1314#eval addPair (3, 4) -- 7Matching on Multiple Inputs
You can pattern match on multiple arguments at once, which is perfect for combining two values or two lists.
1def addOptions : Option Nat → Option Nat → Option Nat2 | some a, some b => some (a + b)3 | _, _ => none45#eval addOptions (some 2) (some 3) -- some 56#eval addOptions none (some 3) -- noneGuards (Conditional Patterns)
Add conditions to patterns with if:
1def classifyNumber (n : Int) : String :=2 match n with3 | 0 => "zero"4 | n => if n > 0 then "positive" else "negative"56-- Or using pattern guards7def grade (score : Nat) : String :=8 match score with9 | s => if s >= 90 then "A"10 else if s >= 80 then "B"11 else if s >= 70 then "C"12 else if s >= 60 then "D"13 else "F"1415#eval grade 85 -- "B"Or-Patterns
Match multiple patterns with the same result:
1def isWeekend : Weekday → Bool2 | Weekday.saturday | Weekday.sunday => true3 | _ => false45-- Without or-patterns (more verbose)6def isWeekend' : Weekday → Bool7 | Weekday.saturday => true8 | Weekday.sunday => true9 | _ => falseAs-Patterns
Use @ to bind a name while also destructuring:
1-- Bind the whole list while also matching head and tail2def processFirst : List Nat → (Nat × List Nat)3 | xs@(x :: _) => (x, xs) -- xs is the whole list, x is first element4 | xs@[] => (0, xs)56#eval processFirst [1, 2, 3] -- (1, [1, 2, 3])78-- Useful when you need both the structure and its parts9def describe : Option Nat → String10 | opt@(some n) => s!"Value {n}, original: {repr opt}"11 | opt@none => s!"None, original: {repr opt}"If-Let and Let-Else
For simple pattern matching with an alternative, use if let:
1-- if let: match a pattern or take else branch2def getFirst (xs : List Nat) : Nat :=3 if let x :: _ := xs then x else 045#eval getFirst [1, 2, 3] -- 16#eval getFirst [] -- 078-- Works with any pattern9def extractValue (opt : Option String) : String :=10 if let some s := opt then s else "default"1112-- let-else: match or diverge early13def processOption (opt : Option Nat) : IO Nat := do14 let some n := opt | return 0 -- Early return if none15 IO.println s!"Processing {n}"16 return n * 2if let when you have one pattern and a simple fallback. Use match when you have multiple cases or complex logic.Deep Dive: Pattern Matching Compiles to Efficient Code
Lean's pattern matching compiles to efficient decision trees. The compiler analyzes patterns and generates optimal branching code—often better than hand-written if-else chains.
This means you can write clear, declarative patterns without worrying about performance.
Matching on Lists
Lists are commonly pattern-matched to process elements:
1def sum : List Nat → Nat2 | [] => 03 | x :: xs => x + sum xs45def head? : List α → Option α6 | [] => none7 | x :: _ => some x89def tail? : List α → Option (List α)10 | [] => none11 | _ :: xs => some xs1213def take : Nat → List α → List α14 | 0, _ => []15 | _, [] => []16 | n + 1, x :: xs => x :: take n xs1718#eval sum [1, 2, 3, 4, 5] -- 1519#eval take 2 [1, 2, 3, 4, 5] -- [1, 2]Write a function that returns both the head and tail of a list if it exists.
1def headTail? : List α → Option (α × List α)2 | [] => none3 | x :: xs => some (x, xs)45#eval headTail? [1, 2, 3] -- some (1, [2, 3])6#eval headTail? ([] : List Nat) -- none