Induction
Induction turns recursive structure into proofs. This advanced lesson shows how to prove properties of natural numbers and lists — and, just as importantly, how to read the errors you get when a proof does not go through.
Learning Goals
- Read the
induction ... withtactic and know what each case owes you. - Tell in advance whether a goal needs induction or is true by computation alone.
- Prove standard facts about
NatandList. - Diagnose a looping
simpand fix it withsimp only.
Induction Is Recursion for Proofs
Level 7 ended with a claim worth making concrete: recursion and induction are the same mechanism. Ask Lean for the type of Nat's recursor and you get the induction principle, exactly as it appears in a maths textbook.
1#check @Nat.rec2-- {motive : Nat → Sort u} →3-- motive Nat.zero →4-- ((n : Nat) → motive n → motive n.succ) →5-- (t : Nat) → motive tRead it as: to produce a motive t for any t, give me the answer for zero, and a way to turn the answer for n into the answer for n + 1. When motive produces data you have written a recursive function; when it produces a proof you have written an induction. The induction tactic is a friendly front-end to this one primitive.
The Shape of an Induction Proof
1theorem zero_add' (n : Nat) : 0 + n = n := by2 induction n with3 | zero => rfl4 | succ k ih =>5 rw [Nat.add_succ, ih]induction nsplits the goal into one case per constructor ofNat.- The
zerocase asks you to prove the statement for0. - The
succcase names the predecessorkand hands youih, the statement already proved fork. - Your job in that case is to get from
ihto the statement fork + 1.
ih. Almost every stuck induction proof becomes obvious once you actually look at what ih says versus what the goal needs.Does This Goal Even Need Induction?
Here is a distinction that catches out nearly everyone, and it is worth internalising before you write another proof. These two statements look symmetric. They are not.
1theorem add_zero (n : Nat) : n + 0 = n := rfl -- no induction needed2theorem zero_add (n : Nat) : 0 + n = n := by -- induction required3 induction n with4 | zero => rfl5 | succ k ih => rw [Nat.add_succ, ih]The reason is the definition of addition. Nat.add recurses on its second argument:
1-- Roughly how Nat.add is defined:2-- n + 0 = n3-- n + (m+1) = (n + m) + 1So n + 0 matches the first equation directly, for any n, with no case analysis at all. It reduces to n by computation, and rfl closes it.
0 + n matches neither equation until you know the shape of n. That is precisely what induction supplies.
rfl. If the goal reduces by computation you are done in one word. Structuring a whole induction around a goal that rfl already closes is a common way to write a proof that is both longer and more fragile than it needs to be.Building Up the Arithmetic
With zero_addin hand you can climb toward commutativity. Each proof uses the previous ones — this stacking is what real formalisation looks like.
1theorem succ_add' (n m : Nat) : (n + 1) + m = (n + m) + 1 := by2 induction m with3 | zero => rfl4 | succ k ih => simp only [Nat.add_succ, ih]56theorem add_comm' (n m : Nat) : n + m = m + n := by7 induction m with8 | zero => rw [Nat.add_zero, Nat.zero_add]9 | succ k ih => rw [Nat.add_succ, ih, Nat.succ_add]When simp Goes Wrong
This deserves its own section because it is the single most confusing failure mode in early Lean proofs, and the error message does not obviously point at the cause. Consider this plausible-looking attempt:
1theorem add_zero_bad (n : Nat) : n + 0 = n := by2 induction n with3 | zero => rfl4 | succ n ih =>5 simp [Nat.add_succ, ih]67-- warning: Possibly looping simp theorem: Nat.add_succ8-- Note: Possibly caused by: Nat.succ_eq_add_one and Nat.add_zero9-- error: Tactic simp failed with a nested error:10-- maximum recursion depth has been reachedThe instinct is to assume Nat.add_succ simply does not apply here. In fact the opposite is true, and that is the whole problem. In the succ case the goal displays as n + 1 + 0 = n + 1, and Lean represents 1 as Nat.succ 0 — so n + 1 genuinely matches the pattern ?a + Nat.succ ?b. The lemma fires on the successor that induction just introduced, then fights the default simp set:
1-- n + 12-- ==[ Nat.add_succ ]==> (n + 0).succ3-- ==[ Nat.succ_eq_add_one ]==> n + 0 + 14-- ==[ Nat.add_zero ]==> n + 15-- ... and around again, foreverNat.succ_eq_add_one and Nat.add_zero are both default simp lemmas, so adding Nat.add_succ closes the cycle. Three fixes, in order of preference:
1-- 1. Does the goal need simp at all? Here it does not.2theorem fix1 (n : Nat) : n + 0 = n := by3 induction n with4 | zero => rfl5 | succ n ih => rfl67-- 2. simp only does not pull in the default simp set.8theorem fix2 (n : Nat) : n + 0 = n := by9 induction n with10 | zero => rfl11 | succ n ih => simp only [Nat.add_succ, ih]1213-- 3. Subtract the offending lemma from the default set.14theorem fix3 (n : Nat) : n + 0 = n := by15 induction n with16 | zero => rfl17 | succ n ih => simp [Nat.add_succ, ih, -Nat.succ_eq_add_one]simp, read the warning above the error. Lean names the looping lemma and usually names its partners too. That warning is the diagnosis; the error is just the symptom.Induction on Lists
Lists have two constructors, so list induction has two cases: nil and cons. The cons case gives you the statement for the tail and asks you to extend it to one more element.
1theorem length_append (xs ys : List α) :2 (xs ++ ys).length = xs.length + ys.length := by3 induction xs with4 | nil => simp5 | cons x xs ih =>6 simp [ih, Nat.succ_add]78theorem map_append (f : α → β) (xs ys : List α) :9 (xs ++ ys).map f = xs.map f ++ ys.map f := by10 induction xs with11 | nil => rfl12 | cons x xs ih => simp [ih]1314theorem append_assoc (xs ys zs : List α) :15 (xs ++ ys) ++ zs = xs ++ (ys ++ zs) := by16 induction xs with17 | nil => rfl18 | cons x xs ih => simp [ih]length_append need that extra Nat.succ_add? Because after simplification the goal is xs.length + ys.length + 1 = xs.length + 1 + ys.length— true, but not something simp closes on its own, since it needs to move the + 1 past the ys.length. Without that lemma you get an “unsolved goals” error showing you exactly this.Proving Things About Your Own Functions
The real payoff is proving properties of functions youwrote. Pass the definition's name to simp and it will unfold the equations.
1def sumList : List Nat → Nat2 | [] => 03 | x :: xs => x + sumList xs45theorem sumList_append (xs ys : List Nat) :6 sumList (xs ++ ys) = sumList xs + sumList ys := by7 induction xs with8 | nil => simp [sumList]9 | cons x xs ih =>10 simp [sumList, ih, Nat.add_assoc]Notice the structure: the induction follows the same [] /x :: xs split that sumListitself was defined by. That is not a coincidence, and it is the most useful heuristic in this level — induct on the same argument your function recurses on.
Deep Dive: Why the induction hypothesis is allowed
Assuming the thing you are trying to prove sounds circular the first time you see it. It is not, because you never assume the statement outright — you assume it for a strictly smaller input, and you only use it to establish a strictly larger one.
Concretely, the zero case gives you the statement for 0 outright. The succ case is a machine that converts a proof for k into a proof for k + 1. Run the machine on the base case and you have 1; run it again and you have 2. For any particular n there is a finite chain from 0 up to it, so a proof exists. Induction just packages all those chains at once.
This is also exactly why Lean's termination checking from Level 7 matters. Both rest on the same fact: you cannot descend through the naturals forever.
Practice
For each goal below, decide first whether rfl alone will close it, then check in the editor.
1example (n : Nat) : n + 0 = n := ?_2example (n : Nat) : 0 + n = n := ?_3example (xs : List Nat) : [] ++ xs = xs := ?_4example (xs : List Nat) : xs ++ [] = xs := ?_Show answers
First and third: rfl. Second and fourth: induction. The pattern is identical in both pairs — ++, like +, is defined by recursion on its first argument for lists, so [] ++ xs reduces immediately while xs ++ [] does not.
1theorem append_nil (xs : List α) : xs ++ [] = xs := by2 induction xs with3 | nil => rfl4 | cons x xs ih => simpProve that mapping a function over a list does not change its length.
Show solution
1theorem length_map (f : α → β) (xs : List α) :2 (xs.map f).length = xs.length := by3 induction xs with4 | nil => rfl5 | cons x xs ih => simp [ih]Define sumTo n as the sum of all naturals up to n, then prove 2 * sumTo n = n * (n + 1). The doubled form avoids division, which keeps everything inside Nat.
Show solution
1def sumTo : Nat → Nat2 | 0 => 03 | n + 1 => (n + 1) + sumTo n45theorem sumTo_formula (n : Nat) : 2 * sumTo n = n * (n + 1) := by6 induction n with7 | zero => rfl8 | succ k ih =>9 simp only [sumTo, Nat.mul_add, Nat.add_mul, Nat.one_mul, Nat.mul_one, ih]10 omega1112#eval sumTo 10 -- 55omega is a decision procedure for linear arithmetic over Nat and Int. It finishes goals that are true by pure arithmetic rearrangement, which saves a lot of tedious rewriting. Note the simp only step first: omega cannot handle products of variables, so we normalise both sides into the same shape before calling it.
Common Mistakes
- Using induction where
rflsuffices. Try the cheap tactic first. - Inducting on the wrong variable. Match the argument your function or operator recurses on.
- Throwing lemmas at
simpuntil it works. That is how you create loops. Read the goal, pick the lemma that matches it. - Ignoring the “possibly looping” warning. It names the culprit before the error even appears.
rfl closes the goal before building anything larger, and reach for simp only when plain simp starts looping.Check Yourself
- Why is
n + 0 = ntrue byrflbut0 + n = nnot? - What exactly does
ihstate in thecons x xs ihcase? - What is the difference between
simpandsimp only? - Given a function that recurses on its second argument, which variable should you induct on?
Continue through the advanced modules to connect induction, type classes, and reusable list proof workflows.
View Advanced Track