List Proofs
Lists are the proving ground for real proof engineering. This final level is less about new concepts than about workflow— how to find the lemma you need, how to choose between tactics, and what to do when a proof will not close.
Learning Goals
- Prove the standard
map,filter, andreverselemmas. - Choose deliberately between
rw,simp, andomega. - Split a proof on a
Boolcondition withby_cases. - Use
simp?andexact?to discover library lemmas. - Debug a stuck proof systematically instead of guessing.
Map and Append
The pattern from Level 8 carries over unchanged: induct on the list the operation recurses on, then feed the induction hypothesis to simp.
1theorem map_append (f : α → β) (xs ys : List α) :2 List.map f (xs ++ ys) = List.map f xs ++ List.map f ys := by3 induction xs with4 | nil => simp5 | cons x xs ih =>6 simp [ih]Map fusion is the same shape, and is one of the more useful rewrites in practice — it turns two traversals into one.
1theorem map_map' (f : α → β) (g : β → γ) (xs : List α) :2 (xs.map f).map g = xs.map (fun x => g (f x)) := by3 induction xs with4 | nil => rfl5 | cons x xs ih => simp [ih]Filter and Length
Filtering can only ever shrink a list. Proving it introduces a genuinely new move: the recursive case depends on whether the predicate accepted the head, so you must split on that.
1theorem length_filter_le (p : α → Bool) (xs : List α) :2 (xs.filter p).length ≤ xs.length := by3 induction xs with4 | nil => simp5 | cons x xs ih =>6 by_cases h : p x7 · simp [h]; omega8 · simp [h]; omegaby_cases h : p x produces two goals: one with h : p x = true and one with h : ¬p x = true. Passing h to simp lets it reduce the filter in each branch.
ih transfers directly. When it rejects, the goal becomes (xs.filter p).length ≤ xs.length + 1 — strictly weaker than ih, so you still need one step to bridge the gap. Writing simp [h, ih] in both branches leaves the second one unsolved.Choosing Your Tactic
Three tactics do most of the work in list proofs. Picking the right one is mostly about how much control you want.
| Tactic | Use when | Watch out for |
|---|---|---|
rw | You know the exact lemma and want one precise rewrite | Fails loudly if the pattern does not match |
simp | You want normalisation and do not care about the route | Can loop; can leave a goal you did not expect |
simp only | You want simp but with a fixed lemma set | Will not finish goals needing default lemmas |
omega | The goal is linear arithmetic on Nat or Int | Cannot handle products of variables |
1-- rw: surgical, names exactly what happens2theorem assoc_by_rw (xs ys zs : List α) :3 (xs ++ ys) ++ zs = xs ++ (ys ++ zs) := by4 rw [List.append_assoc]56-- simp: hand it the induction hypothesis and let it normalise7theorem reverse_append' (xs ys : List α) :8 (xs ++ ys).reverse = ys.reverse ++ xs.reverse := by9 induction xs with10 | nil => simp11 | cons x xs ih => simp [ih]simp, then try tightening the key steps to rw or simp only. Explicit proofs survive library updates far better, because they do not silently depend on whatever happens to be in the default simp set.Finding the Lemma You Need
The hardest part of a real proof is rarely the logic — it is knowing that the fact you want is called List.length_map. Lean will tell you. Both tactics below print a concrete suggestion you can paste back over the call.
1example (xs : List Nat) : (xs.map (· + 1)).length = xs.length := by2 simp?3-- Try this: simp only [List.length_map]45example (n : Nat) : n + 0 = n := by6 exact?7-- Try this: exact Nat.add_eq_left.mpr rflsimp?runssimpand reports the minimal lemma set it actually used.exact?searches the library for a single term that closes the goal.apply?is the same idea but allows leftover goals.
Naming in the standard library is systematic, which makes guessing viable too. Lemma names describe the statement left to right: length_append is about the length of an append; map_map is about a map of a map. Once you know the convention you can often type the name you need and find it exists.
Proving Things About Your Own Functions
Bringing the whole course together: a recursive definition from Level 7, an induction from Level 8, and omega to finish the arithmetic.
1def sumList : List Nat → Nat2 | [] => 03 | x :: xs => x + sumList xs45theorem sumList_map_succ (xs : List Nat) :6 sumList (xs.map (· + 1)) = sumList xs + xs.length := by7 induction xs with8 | nil => rfl9 | cons x xs ih =>10 simp [sumList, ih]11 omegaThe statement says: adding one to every element raises the total by exactly the number of elements. simp unfolds the definitions and applies ih; what remains is a rearrangement of sums that omega settles without you naming a single associativity lemma.
Deep Dive: A checklist for stuck proofs
When a proof will not close, guessing more lemmas is the slowest way out. Work through this instead:
- Read the actual goal. Put the cursor before the failing tactic and look at the Infoview. Most stuck proofs are stuck on a goal different from the one you imagined.
- Compare the goal to
ihterm by term. Name the precise difference. That difference is the lemma you need. - Ask whether you inducted on the right variable. It should be the one your function or operator recurses on.
- Try the cheap closers.
rfl,simp,omega,decide— one of them handles a surprising share of goals. - Then ask the library.
exact?andsimp?. - If
simploops, read the warning, not just the error. It names the offending lemma. Switch tosimp only.
A last resort that is genuinely useful: put sorry in place of the hard step. Lean accepts the proof with a warning, and you can carry on building the rest of the argument, confirming that the shape works before you invest in the detail.
Practice
Prove that reversing a list does not change its length.
Show solution
1theorem length_reverse' (xs : List α) : xs.reverse.length = xs.length := by2 induction xs with3 | nil => rfl4 | cons x xs ih => simpsimp closes the cons case without ih, because the library already knows List.length_reverse. If you pass ihanyway Lean emits an “unused simp argument” warning — a small but useful signal that you were doing more work than the goal required.
Prove that reversing twice gets you back where you started. You will want reverse_appendfrom earlier on the page — think about which form the cons case produces.
Show solution
1theorem reverse_reverse' (xs : List α) : xs.reverse.reverse = xs := by2 induction xs with3 | nil => rfl4 | cons x xs ih => simp [ih]Prove that filtering a concatenation is the same as concatenating the filtered halves. This needs both induction and by_cases.
Show solution
1theorem filter_append' (p : α → Bool) (xs ys : List α) :2 (xs ++ ys).filter p = xs.filter p ++ ys.filter p := by3 induction xs with4 | nil => rfl5 | cons x xs ih =>6 by_cases h : p x7 · simp [h, ih]8 · simp [h, ih]Here the two branches genuinely are symmetric, unlike length_filter_le— the goal is an equation rather than an inequality, so ih applies directly on both sides.
No solution given for this one. Define your own recursive function over lists — count the elements satisfying a predicate, say, or takeWhile— and then state and prove one property of it. Good candidates: the count is at most the length, or the result of takeWhile is a prefix.
Choosing a statement that is true and provable at your current level is itself the skill being exercised. If you get stuck, weaken the statement until you can prove it, then strengthen it again.
Common Mistakes
- Assuming both
by_casesbranches need the same tactic. Check each goal separately. - Adding lemmas to
simpuntil something works. That is how loops appear. Read the goal first. - Reaching for
omegaon a nonlinear goal. It cannot handle products of variables; normalise first. - Ignoring “unused simp argument” warnings. They usually mean the goal was easier than you thought.
simp? and exact? find the lemma. Master that loop and mathlib-scale proof engineering is a matter of degree, not of kind.Where to Go Next
You have covered the core of Lean as both a programming language and a proof assistant. Three natural directions from here:
- Mathlib. The mathematical library, with tactics such as
ring,linarith, andnorm_numthat make the arithmetic in this level much easier. - Metaprogramming. Lean is written in Lean; you can define your own tactics and notation.
- Verified programming. Write real software and prove the parts that matter correct.
Use these examples as templates for small proof refactoring challenges and capstone-style list proofs.
View Advanced Track