Stefano M. Nicoletti

Lecture 7: Induction, Simplification, and Software Verification

1. Aim of the lecture

We continue from the first induction proof in Lecture 6. We prove further properties of addition, introduce simp, and combine induction, rewriting, and simplification. We then apply the same tools to the verification of a recursive algorithm on lists.

2. Definitions and available results

We continue to use the natural numbers, addition, and doubling defined in Lecture 6:

Natural has the constructors zero and succ. The function add (m n : Natural) recurses on its second argument, while double (n : Natural) recurses on its only argument.

The argument examined by a definition determines when Lean can compute. The definition of add can select a branch when it knows the form of the second argument. Thus, add m zero and add m (succ n) reduce directly. By contrast, add zero n requires induction while n remains an unknown variable.

The computational equations and the final result from Lecture 6 are available:

theorem lecture07_add_zero_right (m : Natural) : add m zero = m := m:Natural⊢ add m zero = m All goals completed! 🐙
theorem lecture07_add_succ_right (m n : Natural) : add m (succ n) = succ (add m n) := m:Naturaln:Natural⊢ add m n.succ = (add m n).succ All goals completed! 🐙
theorem lecture07_double_succ (n : Natural) : double (succ n) = succ (succ (double n)) := n:Natural⊢ double n.succ = (double n).succ.succ All goals completed! 🐙
theorem lecture07_zero_add (n : Natural) : add zero n = n := n:Natural⊢ add zero n = n induction n with ⊢ add zero zero = zero All goals completed! 🐙 n:Naturalih:add zero n = n⊢ add zero n.succ = n.succ n:Naturalih:add zero n = n⊢ (add zero n).succ = n.succ n:Naturalih:add zero n = n⊢ n.succ = n.succ All goals completed! 🐙

The first three results state the computational behaviour of the definitions. The last is a general theorem proved by induction. This distinction remains important throughout the lecture: some transformations are computations, whereas others use previously proved results.

3. A second induction proof

We now prove that a successor in the first argument can be moved outside the addition:

add (succ m) n = succ (add m n).

The visible successor may suggest induction on m. The definition tells us otherwise. add examines its second argument, and that position contains n in both additions. The unknown form of n prevents the definition from choosing between its zero and succ branches, so we induct on n.

theorem lecture07_succ_add (m n : Natural) : add (succ m) n = succ (add m n) := m:Naturaln:Natural⊢ add m.succ n = (add m n).succ induction n with m:Natural⊢ add m.succ zero = (add m zero).succ All goals completed! 🐙 m:Naturaln:Naturalih:add m.succ n = (add m n).succ⊢ add m.succ n.succ = (add m n.succ).succ m:Naturaln:Naturalih:add m.succ n = (add m n).succ⊢ (add m.succ n).succ = (add m n).succ.succ m:Naturaln:Naturalih:add m.succ n = (add m n).succ⊢ (add m n).succ.succ = (add m n).succ.succ All goals completed! 🐙

In the base case, replacing n with zero lets both sides reduce to the same expression. In the induction step, unfold add opens both additions for the case succ n. The remaining goal contains the same equality for n, which is exactly the induction hypothesis ih. Rewriting with ih makes both sides identical.

The induction variable therefore follows the computation. We look for the variable in the argument examined by the recursive definition and check that the induction hypothesis will rewrite the recursive call produced in the successor case.

4. Associativity, commutativity, and simp

Associativity states that grouping three addends in different ways does not change the result. On the left, c is the second argument of the outer addition. On the right, it is the second argument of add b c, which in turn controls the outer computation. We therefore induct on c.

theorem lecture07_add_assoc (a b c : Natural) : add (add a b) c = add a (add b c) := a:Naturalb:Naturalc:Natural⊢ add (add a b) c = add a (add b c) induction c with a:Naturalb:Natural⊢ add (add a b) zero = add a (add b zero) All goals completed! 🐙 a:Naturalb:Naturalc:Naturalih:add (add a b) c = add a (add b c)⊢ add (add a b) c.succ = add a (add b c.succ) a:Naturalb:Naturalc:Naturalih:add (add a b) c = add a (add b c)⊢ (add (add a b) c).succ = add a (add b c.succ) a:Naturalb:Naturalc:Naturalih:add (add a b) c = add a (add b c)⊢ (add (add a b) c).succ = add a (add b c).succ a:Naturalb:Naturalc:Naturalih:add (add a b) c = add a (add b c)⊢ (add (add a b) c).succ = (add a (add b c)).succ All goals completed! 🐙

In the base case, additions with zero as their second argument reduce, and rfl closes the goal. In the successor case, three rewrites with lecture07_add_succ_right expose the recursive additions on both sides. The remaining equality is the induction hypothesis under succ, so rw [ih] completes the proof.

Commutativity states that exchanging the two addends does not change the result. It does not follow from computation alone because add treats its two arguments asymmetrically: it examines the second argument and leaves the first unchanged. We induct on b. In the base case, the left side computes while the right side uses lecture07_zero_add. In the successor case, lecture07_add_succ_right handles the successor on the right and lecture07_succ_add handles the successor that appears in the first argument after the exchange.

theorem lecture07_add_comm (a b : Natural) : add a b = add b a := a:Naturalb:Natural⊢ add a b = add b a induction b with a:Natural⊢ add a zero = add zero a a:Natural⊢ add a zero = a All goals completed! 🐙 a:Naturalb:Naturalih:add a b = add b a⊢ add a b.succ = add b.succ a a:Naturalb:Naturalih:add a b = add b a⊢ (add a b).succ = add b.succ a a:Naturalb:Naturalih:add a b = add b a⊢ (add a b).succ = (add b a).succ All goals completed! 🐙

This proof explains why small supporting lemmas matter. The definition gives the equations for the second argument directly. The theorems zero_add and succ_add provide the corresponding forms for the first argument. Commutativity combines these two views.

When a proof requires many elementary rewrites, we can use the simplifier. simp applies equations oriented towards simpler forms and performs basic computations that Lean recognizes directly. It does not search arbitrarily for a proof. It uses a collection of rules called the simp-set.

We compare three uses of the same theorem. rw performs a specific rewrite chosen by us. simp only [lemmas] uses only the listed lemmas, together with the simplifier's essential reductions. simp [lemma] uses the standard rules and adds lemma for that command.

theorem lecture07_zero_add_with_rw (n : Natural) : add zero n = n := n:Natural⊢ add zero n = n All goals completed! 🐙

The first theorem names the exact rewrite. The second asks the simplifier to work with one declared rule. The third adds the theorem to the simp-set for a single invocation. All three prove the same statement, but they provide different degrees of control and automation.

theorem lecture07_zero_add_with_simp_only (n : Natural) : add zero n = n := n:Natural⊢ add zero n = n All goals completed! 🐙
theorem lecture07_zero_add_with_simp (n : Natural) : add zero n = n := n:Natural⊢ add zero n = n All goals completed! 🐙
theorem lecture07_succ_add_with_simp (m n : Natural) : add (succ m) n = succ (add m n) := m:Naturaln:Natural⊢ add m.succ n = (add m n).succ All goals completed! 🐙

The local simp attribute applies only inside the section. It lets us extend the simp-set without changing every later proof in the module. Inside the section, simp finds lecture07_succ_add automatically, so the theorem need not be named in the tactic call.

@[simp] theorem lecture07_double_zero : double zero = zero := ⊢ double zero = zero All goals completed! 🐙

@[simp] registers a rule for the rest of the module. We use it for a canonical equation such as double zero = zero. Associativity and commutativity are not appropriate simp rules because they only move parentheses or swap arguments.

Rule selection matters. A good simplification rule predictably moves an expression towards a shorter or canonical form. Associativity would require a global choice of grouping. Commutativity could keep exchanging arguments without producing an evidently simpler expression. We therefore apply these results explicitly where they are needed.

5. A final proof

We combine induction and rewriting to show that the double of a sum is the sum of the doubles. The subexpression that remains blocked is add m n, so we induct on n.

theorem lecture07_double_add (m n : Natural) : double (add m n) = add (double m) (double n) := m:Naturaln:Natural⊢ double (add m n) = add (double m) (double n) induction n with m:Natural⊢ double (add m zero) = add (double m) (double zero) All goals completed! 🐙 m:Naturaln:Naturalih:double (add m n) = add (double m) (double n)⊢ double (add m n.succ) = add (double m) (double n.succ) m:Naturaln:Naturalih:double (add m n) = add (double m) (double n)⊢ double (add m n).succ = add (double m) (double n.succ) m:Naturaln:Naturalih:double (add m n) = add (double m) (double n)⊢ (double (add m n)).succ.succ = add (double m) (double n.succ) m:Naturaln:Naturalih:double (add m n) = add (double m) (double n)⊢ (double (add m n)).succ.succ = add (double m) (double n).succ.succ m:Naturaln:Naturalih:double (add m n) = add (double m) (double n)⊢ (double (add m n)).succ.succ = (add (double m) (double n).succ).succ m:Naturaln:Naturalih:double (add m n) = add (double m) (double n)⊢ (double (add m n)).succ.succ = (add (double m) (double n)).succ.succ All goals completed! 🐙

The base case follows from the definitions of add and double. In the successor case, the first rewrite moves the successor out of add m (succ n). The two rewrites with lecture07_double_succ describe doubling a successor on both sides. The next two addition rewrites expose the same successor structure. Finally, ih replaces the double of the smaller sum by the sum of the doubles.

After checking each step, we can express the same proof with a controlled collection of simplification rules:

theorem lecture07_double_add_short (m n : Natural) : double (add m n) = add (double m) (double n) := m:Naturaln:Natural⊢ double (add m n) = add (double m) (double n) induction n with m:Natural⊢ double (add m zero) = add (double m) (double zero) All goals completed! 🐙 m:Naturaln:Naturalih:double (add m n) = add (double m) (double n)⊢ double (add m n.succ) = add (double m) (double n.succ) All goals completed! 🐙

The shorter version does not use a different mathematical argument. It uses the same equations but lets simp only choose their occurrences. The explicit list remains useful because it records which results justify the proof.

6. From induction to software verification

A program takes an input and produces an output. Verification states an expected property precisely and proves it for every input. To apply this idea to a new data structure, we begin by defining the type of lists inductively.

inductive CustomList (α : Type) where
  | empty
  | cons (head : α) (tail : CustomList α)

A list is therefore empty or built from a first element and a smaller list. In the next lecture, we will use this structure to define an algorithm and verify some of its properties.

Sources