Module 4 · Level 10 · Advanced

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.

This lesson pulls together induction, rewriting, simplification, and case splits.

Learning Goals

  • Prove the standard map, filter, and reverse lemmas.
  • Choose deliberately between rw, simp, and omega.
  • Split a proof on a Bool condition with by_cases.
  • Use simp? and exact? 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.

lean
1theorem map_append (f : α β) (xs ys : List α) :
2 List.map f (xs ++ ys) = List.map f xs ++ List.map f ys := by
3 induction xs with
4 | nil => simp
5 | 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.

lean
1theorem map_map' (f : α β) (g : β γ) (xs : List α) :
2 (xs.map f).map g = xs.map (fun x => g (f x)) := by
3 induction xs with
4 | nil => rfl
5 | 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.

lean
1theorem length_filter_le (p : α Bool) (xs : List α) :
2 (xs.filter p).length xs.length := by
3 induction xs with
4 | nil => simp
5 | cons x xs ih =>
6 by_cases h : p x
7 · simp [h]; omega
8 · simp [h]; omega

by_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.

The two branches are not symmetric, which is easy to miss. When the predicate accepts, both sides grow by one and 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.

TacticUse whenWatch out for
rwYou know the exact lemma and want one precise rewriteFails loudly if the pattern does not match
simpYou want normalisation and do not care about the routeCan loop; can leave a goal you did not expect
simp onlyYou want simp but with a fixed lemma setWill not finish goals needing default lemmas
omegaThe goal is linear arithmetic on Nat or IntCannot handle products of variables
lean
1-- rw: surgical, names exactly what happens
2theorem assoc_by_rw (xs ys zs : List α) :
3 (xs ++ ys) ++ zs = xs ++ (ys ++ zs) := by
4 rw [List.append_assoc]
5
6-- simp: hand it the induction hypothesis and let it normalise
7theorem reverse_append' (xs ys : List α) :
8 (xs ++ ys).reverse = ys.reverse ++ xs.reverse := by
9 induction xs with
10 | nil => simp
11 | cons x xs ih => simp [ih]
💡
A good habit for proofs you intend to keep: get it working with 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.

lean
1example (xs : List Nat) : (xs.map (· + 1)).length = xs.length := by
2 simp?
3-- Try this: simp only [List.length_map]
4
5example (n : Nat) : n + 0 = n := by
6 exact?
7-- Try this: exact Nat.add_eq_left.mpr rfl
  • simp? runs simp and 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.

lean
1def sumList : List Nat Nat
2 | [] => 0
3 | x :: xs => x + sumList xs
4
5theorem sumList_map_succ (xs : List Nat) :
6 sumList (xs.map (· + 1)) = sumList xs + xs.length := by
7 induction xs with
8 | nil => rfl
9 | cons x xs ih =>
10 simp [sumList, ih]
11 omega

The 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 ih term 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? and simp?.
  • If simp loops, read the warning, not just the error. It names the offending lemma. Switch to simp 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

Exercise 1: Reverse Preserves Length

Prove that reversing a list does not change its length.

Show solution
lean
1theorem length_reverse' (xs : List α) : xs.reverse.length = xs.length := by
2 induction xs with
3 | nil => rfl
4 | cons x xs ih => simp

simp 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.

Exercise 2: Reverse Is an Involution

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
lean
1theorem reverse_reverse' (xs : List α) : xs.reverse.reverse = xs := by
2 induction xs with
3 | nil => rfl
4 | cons x xs ih => simp [ih]
Exercise 3: Filter Distributes Over Append

Prove that filtering a concatenation is the same as concatenating the filtered halves. This needs both induction and by_cases.

Show solution
lean
1theorem filter_append' (p : α Bool) (xs ys : List α) :
2 (xs ++ ys).filter p = xs.filter p ++ ys.filter p := by
3 induction xs with
4 | nil => rfl
5 | cons x xs ih =>
6 by_cases h : p x
7 · 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.

Exercise 4: Capstone

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_cases branches need the same tactic. Check each goal separately.
  • Adding lemmas to simp until something works. That is how loops appear. Read the goal first.
  • Reaching for omega on 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.
Key Takeaway
List proofs build on induction and rewriting, but the skill that scales is workflow: read the real goal, compare it to the induction hypothesis, name the difference, and let 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, and norm_num that 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.
Advanced Track

Use these examples as templates for small proof refactoring challenges and capstone-style list proofs.

View Advanced Track