Formal Software Verification

4.6. Proofs by Mathematical Induction🔗

The tactic induction performs structural induction on a variable, producing one named subgoal per constructor of its type. For ℕ, built from Nat.zero and Nat.succ, structural induction is ordinary mathematical induction. The names after a constructor bind its arguments and the induction hypothesis, so the branch | succ n' ih provides the predecessor n' and the hypothesis ih about it. The general form for ℕ reads as follows.

induction n with
| zero       => (proof of the base case)
| succ n' ih => (proof of the step case)

The section recalls add and mul from Lecture 3 and proves the laws that computation left open there. The extracted exercise file repeats the definitions, so it stands alone.

namespace Backward def add : | m, Nat.zero => m | m, Nat.succ n => Nat.succ (add m n) def mul : | _, Nat.zero => 0 | m, Nat.succ n => add m (mul m n) end Backward

The first two theorems supply the equations for add when its first argument is zero or a successor, which the definition itself does not give, since add recurses on its second argument. Each proof inducts on that second argument, the variable the recursion consumes, and closes the step case with simp, using the defining equations of add and the induction hypothesis.

namespace Backward theorem add_zero (n : ) : add 0 n = n := n:add 0 n = n induction n with add 0 0 = 0 All goals completed! 🐙 n':ih:add 0 n' = n'add 0 (n' + 1) = n' + 1 All goals completed! 🐙 theorem add_succ (m n : ) : add (Nat.succ m) n = Nat.succ (add m n) := m:n:add m.succ n = (add m n).succ induction n with m:add m.succ 0 = (add m 0).succ All goals completed! 🐙 m:n':ih:add m.succ n' = (add m n').succadd m.succ (n' + 1) = (add m (n' + 1)).succ All goals completed! 🐙 end Backward

Commutativity and associativity follow, with the two theorems above discharging the base and step cases of the first. They re-prove the propositions stated with sorry as SorryTheorems.add_comm and SorryTheorems.add_assoc in Lecture 3, here as the new theorems Backward.add_comm and Backward.add_assoc. The earlier declarations keep their sorryAx proofs, and other Lecture 3 statements, among them mul_comm, mul_assoc and reverse_reverse, stay open.

namespace Backward theorem add_comm (m n : ) : add m n = add n m := m:n:add m n = add n m induction n with m:add m 0 = add 0 m All goals completed! 🐙 m:n':ih:add m n' = add n' madd m (n' + 1) = add (n' + 1) m All goals completed! 🐙 theorem add_assoc (l m n : ) : add (add l m) n = add l (add m n) := l:m:n:add (add l m) n = add l (add m n) induction n with l:m:add (add l m) 0 = add l (add m 0) All goals completed! 🐙 l:m:n':ih:add (add l m) n' = add l (add m n')add (add l m) (n' + 1) = add l (add m (n' + 1)) All goals completed! 🐙 end Backward

The two instances below register add as associative and commutative, which is what ac_rfl consults. The instance command is the one Lecture 2 used for Membership and its companions, and chapter 5 of the guide explains the mechanism, in week 6 of the course.

namespace Backward instance Associative_add : Std.Associative add := { assoc := add_assoc } instance Commutative_add : Std.Commutative add := { comm := add_comm } end Backward

Distributivity closes the section, with ac_rfl finishing what simp leaves.

namespace Backward theorem mul_add (l m n : ) : mul l (add m n) = add (mul l m) (mul l n) := l:m:n:mul l (add m n) = add (mul l m) (mul l n) induction n with l:m:mul l (add m 0) = add (mul l m) (mul l 0) All goals completed! 🐙 l:m:n':ih:mul l (add m n') = add (mul l m) (mul l n')mul l (add m (n' + 1)) = add (mul l m) (mul l (n' + 1)) l:m:n':ih:mul l (add m n') = add (mul l m) (mul l n')add l (add (mul l m) (mul l n')) = add (mul l m) (add l (mul l n')) All goals completed! 🐙 end Backward

The guide offers two hints. Induct on the argument the recursion consumes, and read a difficult base case as a sign of the wrong induction variable or of a missing auxiliary theorem.

[addzero]The guide names add 0 n = n as add_zero even though add recurses on its second argument and the usual convention reads the zero from the statement, which would give zero_add. The third worked example of Lecture 3 called the same statement zero_add. These notes keep the guide's names.

4.6.1. Examples🔗

The examples below induct on ℕ and once on lists, watch the two subgoals, and check what the finished proofs rest on. In the goals, Lean prints Nat.succ n' as n' + 1 and uses the built-in +, not our add, so a trace that shows n' + 1 reflects the pretty printer, not a change of definition.

Example 1. induction n with produces one branch per constructor, and the trace shows the base and the step goals.

example (n : ) : add 0 n = n := n:add 0 n = n induction n with add 0 0 = 0 add 0 0 = 0add 0 0 = 0 All goals completed! 🐙 n':ih:add 0 n' = n'add 0 (n' + 1) = n' + 1 n':ih:add 0 n' = n'add 0 (n' + 1) = n' + 1n':ih:add 0 n' = n'add 0 (n' + 1) = n' + 1 All goals completed! 🐙
add 0 0 = 0
n':ih:add 0 n' = n'add 0 (n' + 1) = n' + 1

Example 2. The base case alone. Zero on the right matches the first equation of add, so rfl closes it.

example : add 0 0 = 0 := add 0 0 = 0 All goals completed! 🐙

Example 3. The step case alone, from its induction hypothesis.

example (n' : ) (ih : add 0 n' = n') : add 0 (Nat.succ n') = Nat.succ n' := n':ih:add 0 n' = n'add 0 n'.succ = n'.succ All goals completed! 🐙

Example 4. add_succ follows the same pattern on a statement with two variables, inducting on the second, which the recursion consumes.

example (m n : ) : add (Nat.succ m) n = Nat.succ (add m n) := m:n:add m.succ n = (add m n).succ induction n with m:add m.succ 0 = (add m 0).succ All goals completed! 🐙 m:n':ih:add m.succ n' = (add m n').succadd m.succ (n' + 1) = (add m (n' + 1)).succ All goals completed! 🐙

Example 5. Associativity, inducting on the last variable.

example (l m n : ) : add (add l m) n = add l (add m n) := l:m:n:add (add l m) n = add l (add m n) induction n with l:m:add (add l m) 0 = add l (add m 0) All goals completed! 🐙 l:m:n':ih:add (add l m) n' = add l (add m n')add (add l m) (n' + 1) = add l (add m (n' + 1)) All goals completed! 🐙

Example 6. The wrong induction variable stalls the naive script. Inducting on m leaves goals that neither rfl nor the induction hypothesis closes, and the traces show why: the recursion of add consumes n, which both goals leave untouched. The statement is still provable, since add_succ above is exactly it, but the base-and-step routine of the earlier proofs does not carry through here.

declaration uses `sorry`example (m n : ) : add (Nat.succ m) n = Nat.succ (add m n) := m:n:add m.succ n = (add m n).succ induction m with n:add (Nat.succ 0) n = (add 0 n).succ n:add (Nat.succ 0) n = (add 0 n).succn:add (Nat.succ 0) n = (add 0 n).succ All goals completed! 🐙 n:m':ih:add m'.succ n = (add m' n).succadd (m' + 1).succ n = (add (m' + 1) n).succ n m':ih:add m'.succ n = (add m' n).succadd (m' + 1).succ n = (add (m' + 1) n).succn:m':ih:add m'.succ n = (add m' n).succadd (m' + 1).succ n = (add (m' + 1) n).succ All goals completed! 🐙
n:add (Nat.succ 0) n = (add 0 n).succ
n m':ih:add m'.succ n = (add m' n).succadd (m' + 1).succ n = (add (m' + 1) n).succ

Example 7. With the two instances registered, ac_rfl reasons about add as it reasons about +.

example (a b c : ) : add (add a b) c = add c (add b a) := a:b:c:add (add a b) c = add c (add b a) All goals completed! 🐙

Example 8. The recursive equation of mul on its first argument, by induction on the second.

example (n : ) : mul 0 n = 0 := n:mul 0 n = 0 induction n with mul 0 0 = 0 All goals completed! 🐙 n':ih:mul 0 n' = 0mul 0 (n' + 1) = 0 All goals completed! 🐙

Example 9. Induction on a list has one branch per constructor of List, with nil as the base and cons as the step. Weeks 6 and 7 treat structural induction on arbitrary inductive types.

namespace Backward theorem append_nil {α : Type} (xs : List α) : appendPretty xs [] = xs := α:Typexs:List αappendPretty xs [] = xs induction xs with α:TypeappendPretty [] [] = [] All goals completed! 🐙 α:Typex:αxs':List αih:appendPretty xs' [] = xs'appendPretty (x :: xs') [] = x :: xs' All goals completed! 🐙 end Backward

Example 10. The finished proof rests on propext, which simp uses, and not on sorryAx, closing the loop with the third example of Lecture 3's theorem section.

'Backward.add_comm' depends on axioms: [propext]#print axioms Backward.add_comm
'Backward.add_comm' depends on axioms: [propext]