Logical Foundations

7. Tactics: More Basic Tactics🔗

Note to developers (before next release)

This chapter could maybe use one or two more WORKINCLASS tags...

Note to developers (Benjamin Pierce @bcpierce00, before next release, 2025)

General comment: All the previous chapters have felt pretty smooth. This one suddenly feels like we're throwing a huge amount of information at them, with little scaffolding -- just a bunch of miscellaneous tactics and examples. Wish it flowed better, somehow.

import LF.Poly
import LF.CustomTactics

7.1. Tactics injection and contradiction🔗

The constructors of inductive types are injective (one-to-one) and disjoint. E.g., for Nat:

  • if n + 1 = m + 1 then it must be that n = m

  • 0 is not equal to n + 1 for any n

7.1.1. Injectivity🔗

We can prove the injectivity of Nat.succ by using the Nat.pred function:

example (n m : Nat) (h : n + 1 = m + 1) : n = m := n:Natm:Nath:n + 1 = m + 1⊢ n = m n:Natm:Nath:n + 1 = m + 1this:n = (n + 1).pred⊢ n = m /- The hypothesis name defaults to `this` when unspecified. -/ n:Natm:Nath:n + 1 = m + 1this:n = (n + 1).pred⊢ (m + 1).pred = m All goals completed! 🐙

As a convenience, the injection tactic allows us to exploit injectivity of any constructor (not just Nat.succ).

example (n m : Nat) (h : n + 1 = m + 1) : n = m := n:Natm:Nath:n + 1 = m + 1⊢ n = m All goals completed! 🐙

When the generated equations do not immediately close the goal, the equations are added to the context instead; adding with allows us to explicitly name the equations (otherwise Lean generates names for us).

declaration uses `sorry`example (n m o : Nat) (h : [n, m] = [o, o]) : n = m := n:Natm:Nato:Nath:[n, m] = [o, o]⊢ n = m All goals completed! 🐙

There is also a related tactic, injections, that applies the injection tactic to all hypotheses, repeatedly. Using it simplifies the proof of the above example.

declaration uses `sorry`example (n m o : Nat) (h : [n, m] = [o, o]) : n = m := n:Natm:Nato:Nath:[n, m] = [o, o]⊢ n = m All goals completed! 🐙

Note that both injection and injections will simplify a hypothesis before applying injectivity. Thus we could also use them to solve the following example, which requires simplifying the ++ and List.reverse expressions:

example (n m o : Nat) (h : [n] ++ [m] = List.reverse ([o] ++ [o])) : n = m := n:Natm:Nato:Nath:[n] ++ [m] = ([o] ++ [o]).reverse⊢ n = m n:Natm:Nato:Nath₁:n = oh₃:m = o⊢ n = m All goals completed! 🐙

7.1.2. Disjointness🔗

Two terms beginning with different constructors (like 0 and Nat.succ, or true and false) can never be equal.

The contradiction tactic embodies this principle. If the context contains a contradictory hypothesis, such as false = true, contradiction solves the current goal immediately. Some examples:

example (n m : Nat) (h : false = true) : n = m := n:Natm:Nath:false = true⊢ n = m All goals completed! 🐙 example (n : Nat) (h : n + 1 = 0) : 2 + 2 = 5 := n:Nath:n + 1 = 0⊢ 2 + 2 = 5 All goals completed! 🐙

These examples are instances of a logical principle known as the principle of explosion, which asserts that a contradictory hypothesis entails anything — even manifestly false things!

Note to developers (Mike Hicks @mwhicks1)

Is there a way to relate this to the interpretation of implication P -> Q where it is true when P is false and Q is true? It seems like maybe this is a computational interpretation so perhaps not.

Sometimes you need to do a little work to expose a contradictory hypothesis involving constructors.

example (n : Nat) (h : 1 + n = 0) : 2 + 2 = 5 := n:Nath:1 + n = 0⊢ 2 + 2 = 5 Tactic `contradiction` failed n:Nath:1 + n = 0⊢ 2 + 2 = 5n:Nath:1 + n = 0⊢ 2 + 2 = 5 -- doesn't work because `1 + n` doesn't reduce to `n.succ`.

To fix it, rewriting with Nat.one_add changes the hypothesis from 1 + n = 0 to n.succ = 0. Then Lean can immediately recognize this as impossible.

example (n : Nat) (h : 1 + n = 0) : 2 + 2 = 5 := n:Nath:1 + n = 0⊢ 2 + 2 = 5 n:Nath:n.succ = 0⊢ 2 + 2 = 5 All goals completed! 🐙

7.1.3. Quizzes🔗

Recall our RGB and Color types:

inductive RGB : Type where | red | green | blueinductive Color : Type where | black | white | primary (p: RGB)
Quiz

Suppose Lean's proof state looks like

x : RGB
y : RGB
h : .primary x = .primary y
------------------------------
⊢ y = x

and we apply the tactic injection h with hxy. What will happen?

(1) "No goals."

(2) The tactic fails.

(3) Lean adds a hypothesis hxy : x = y, while the goal remains y = x.

(4) None of the above.

Show solution

(3)

example (x y : RGB) (h : Color.primary x = Color.primary y) : y = x := x:RGBy:RGBh:Color.primary x = Color.primary y⊢ y = x x:RGBy:RGBhxy:x = y⊢ y = x x:RGBy:RGBhxy:x = y⊢ x = y All goals completed! 🐙
Quiz

Suppose Lean's proof state looks like

x : Bool
y : Bool
h : !x = !y
--------------
⊢ y = x

and we apply the tactic injection h with hxy. What will happen?

(A) "No more goals."

(B) The tactic fails.

(C) Hypothesis h becomes hxy : x = y.

(D) None of the above.

Show solution
example (x y : Bool) (h : !x = !y) : y = x := x:Booly:Boolh:(!decide (x = !y)) = true⊢ y = x Tactic `injection` failed: equality of constructor applications expected x y:Boolh:(!decide (x = !y)) = true⊢ y = xx:Booly:Boolh:(!decide (x = !y)) = true⊢ y = x
Tactic `injection` failed: equality of constructor applications expected

x y:Boolh:(!decide (x = !y)) = true⊢ y = x
Quiz

Now suppose Lean's proof state looks like

x : Nat
y : Nat
h : x + 1 = y + 1
-------------------
⊢ y = x

and we apply the tactic injection h with hxy. What will happen?

(A) "No more goals."

(B) The tactic fails.

(C) Hypothesis h becomes hxy : x = y.

(D) None of the above.

Show solution
example (x y : Nat) (h : x + 1 = y + 1) : y = x := x:Naty:Nath:x + 1 = y + 1⊢ y = x x:Naty:Nathxy:x = y⊢ y = x x:Naty:Nathxy:x = y⊢ x = y All goals completed! 🐙
Quiz

Finally, suppose Lean's proof state looks like

x : Nat
y : Nat
h : 1 + x = 1 + y
-------------------
⊢ y = x

and we apply the tactic injection h with hxy. What will happen?

(A) "No more goals."

(B) The tactic fails.

(C) Hypothesis h becomes hxy : x = y.

(D) None of the above.

Show solution
example (x y : Nat) (h : 1 + x = 1 + y) : y = x := x:Naty:Nath:1 + x = 1 + y⊢ y = x Tactic `injection` failed: equality of constructor applications expected x y:Nath:1 + x = 1 + y⊢ y = xx:Naty:Nath:1 + x = 1 + y⊢ y = x
Tactic `injection` failed: equality of constructor applications expected

x y:Nath:1 + x = 1 + y⊢ y = x

The addition in 1 + x (and 1 + y) is blocked by the variable in the second argument. Therefore it doesn't reduce to x.succ, so injectivity of constructors can't be used directly.

7.1.4. Tactic congr🔗

The injectivity of constructors allows us to reason that ∀ (n m : Nat), n + 1 = m + 1 → n = m. The converse of this implication also holds:

example (n m : Nat) (h : n = m) : n + 1 = m + 1 := n:Natm:Nath:n = m⊢ n + 1 = m + 1 All goals completed! 🐙
Note to developers (Mike Hicks @mwhicks1)

Is the general "fact" highlighted below a principle of congruence ? Let's say so if that's the case.

This is an instance of a more general fact about both constructors and functions:

Note to developers (Benjamin Pierce @bcpierce00)

The fact that the earlier conversation was only about constructors, not functions, was never explicitly called out. It would be good to do so.

example {α β : Type} (f : α → β) (x y : α) (h : x = y) : f x = f y := α:Typeβ:Typef:α → βx:αy:αh:x = y⊢ f x = f y All goals completed! 🐙

Lean also provides congr as a tactic.

example (n m : Nat) (h : n = m) : n + 1 = m + 1 := n:Natm:Nath:n = m⊢ n + 1 = m + 1 All goals completed! 🐙

We can specify the recursion-depth with congr n.

example (a b c d : Nat) (h1 : a = b + 1) (h2 : d = c + 1) : (a + c, true) = (b + d, true) := unsolved goals a b c d:Nath1:a = b + 1h2:d = c + 1⊢ a = b a b c d:Nath1:a = b + 1h2:d = c + 1⊢ c = da:Natb:Natc:Natd:Nath1:a = b + 1h2:d = c + 1⊢ (a + c, true) = (b + d, true) a:Natb:Natc:Natd:Nath1:a = b + 1h2:d = c + 1⊢ a = ba:Natb:Natc:Natd:Nath1:a = b + 1h2:d = c + 1⊢ c = d

We now have two goals: a = b and c = d, but these are not provable from our hypotheses! congr has gone too deep.

unsolved goals
a b c d:Nath1:a = b + 1h2:d = c + 1⊢ a = b

a b c d:Nath1:a = b + 1h2:d = c + 1⊢ c = d
example (a b c d : Nat) (h1 : a = b + 1) (h2 : d = c + 1) : (a + c, true) = (b + d, true) := a:Natb:Natc:Natd:Nath1:a = b + 1h2:d = c + 1⊢ (a + c, true) = (b + d, true) /- Using `congr` shallowly allows us to complete the proof -/ a:Natb:Natc:Natd:Nath1:a = b + 1h2:d = c + 1⊢ a + c = b + d All goals completed! 🐙

7.2. Using cases on Expressions🔗

The cases tactic can be used on expressions as well as variables:

def chooseIf {α : Type} (test : α → Bool) (x y : α) : α := bif test x then x else y theorem chooseIf_self {α : Type} (test : α → Bool) (x : α) : chooseIf test x x = x := α:Typetest:α → Boolx:α⊢ chooseIf test x x = x α:Typetest:α → Boolx:α⊢ (bif test x then x else x) = x α:Typetest:α → Boolx:α⊢ (bif false then x else x) = xα:Typetest:α → Boolx:α⊢ (bif true then x else x) = x α:Typetest:α → Boolx:α⊢ (bif false then x else x) = xα:Typetest:α → Boolx:α⊢ (bif true then x else x) = x All goals completed! 🐙

7.2.1. Destructing Tuples🔗

The cases tactic is useful when we are dealing with values that can be one of a list of things (a Bool is either a false or a true, a Nat is either 0 or succ n, etc.). When we want more information about a value that is a tuple of multiple things, we instead want a way to extract the pieces of that value.

If we have a value v : α × β in our context, we can extract the first and second components of v and give them names using this tactic:

let ⟨a, b⟩ := v

7.2.2. Splitting with Equations🔗

When using cases, we can specify to Lean that it should remember an equality between a compound expression and what we are decomposing it into, using cases h : ... syntax. This step is sometimes critical: if we leave it out, we might lack information we need to complete a proof.

def keepIf {α : Type} (test : α → Bool) (x : α) : Option α := bif test x then some x else none

Adding the h : ⋯ qualifier saves this information so we can use it.

theorem keepIf_some {α : Type} (test : α → Bool) (x y : α) (h : keepIf test x = some y) : x = y := α:Typetest:α → Boolx:αy:αh:keepIf test x = some y⊢ x = y α:Typetest:α → Boolx:αy:αh:(bif test x then some x else none) = some y⊢ x = y -- Now we have the same state as at the point where we got stuck -- above, except that the context contains an extra equality -- assumption, which is exactly what we need to make progress. cases hTest : test x with α:Typetest:α → Boolx:αy:αh:(bif test x then some x else none) = some yhTest:test x = false⊢ x = y α:Typetest:α → Boolx:αy:αh:(bif false then some x else none) = some yhTest:test x = false⊢ x = y All goals completed! 🐙 α:Typetest:α → Boolx:αy:αh:(bif test x then some x else none) = some yhTest:test x = true⊢ x = y α:Typetest:α → Boolx:αy:αh:(bif true then some x else none) = some yhTest:test x = true⊢ x = y All goals completed! 🐙

7.3. The apply Tactic🔗

The apply tactic is useful when the goal is instead the conclusion of an implication. If the conclusion of the implication matches the current goal, its premises become new subgoals to be proved.

example (p q : Prop) (h : p → q) (hp : p) : q := p:Propq:Proph:p → qhp:p⊢ q p:Propq:Proph:p → qhp:p⊢ p All goals completed! 🐙

Another example:

example (n m o p : Nat) (hnm : n = m) (h : n = m → [n, o] = [m, p]) : [n, o] = [m, p] := n:Natm:Nato:Natp:Nathnm:n = mh:n = m → [n, o] = [m, p]⊢ [n, o] = [m, p] n:Natm:Nato:Natp:Nathnm:n = mh:n = m → [n, o] = [m, p]⊢ n = m All goals completed! 🐙

This process is called backward reasoning. We are trying to prove some goal ⊢ b and we know some fact h : a → b. So we work backwards by applying that fact, which replaces the goal with ⊢ a.

Observe how Lean picks appropriate values for the universally quantified variables of the hypothesis:

example (n m : Nat) (h₁ : (n, n) = (m, m)) (h₂ : ∀ (q r : Nat), (q, q) = (r, r) → [q] = [r]) : [n] = [m] := n:Natm:Nath₁:(n, n) = (m, m)h₂:∀ (q r : Nat), (q, q) = (r, r) → [q] = [r]⊢ [n] = [m] n:Natm:Nath₁:(n, n) = (m, m)h₂:∀ (q r : Nat), (q, q) = (r, r) → [q] = [r]⊢ (n, n) = (m, m) All goals completed! 🐙

The goal must match the hypothesis for apply to work:

example (n m : Nat) (h : n = 0 → n = m) (hn : n = 0) : m = n := n:Natm:Nath:n = 0 → n = mhn:n = 0⊢ m = n /- Here we cannot use `apply` directly... ...but we can use the `symm` tactic, which switches the left and right sides of an equality in the goal. -/ n:Natm:Nath:n = 0 → n = mhn:n = 0⊢ n = m n:Natm:Nath:n = 0 → n = mhn:n = 0⊢ n = 0 All goals completed! 🐙
Note to developers (Mike Hicks @mwhicks1)

The above example introduces the symm tactic as a sort of aside. It would be nice if this were made more evident in the TOC for the chapter, for easier searching.

7.3.1. Supplying arguments to apply🔗

The following silly example uses two rewrites in a row to get from [u, v] to [y, z].

example (u v w x y z : Nat) (h₁ : [u, v] = [w, x]) (h₂ : [w, x] = [y, z]) : [u, v] = [y, z] := u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [u, v] = [y, z] All goals completed! 🐙

Since this is a common pattern, we might like to pull it out as a lemma that records, once and for all, the fact that equality is transitive.

theorem trans_eq {α : Type} (a b c : α) : a = b → b = c → a = c := α:Typea:αb:αc:α⊢ a = b → b = c → a = c α:Typea:αb:αc:αh₁:a = bh₂:b = c⊢ a = c All goals completed! 🐙

Lean already provides exactly this theorem as Eq.trans:

Eq.trans.{u} {α : Sort u} {a b c : α} (h₁ : a = b) (h₂ : b = c) : a = c#check Eq.trans
Eq.trans.{u} {α : Sort u} {a b c : α} (h₁ : a = b) (h₂ : b = c) : a = c
Note to developers (Mike Hicks @mwhicks1)

The above #check shows Lean's use of sorts, which we have seen before and not explained. When is a good time to actually explain this? The Logic chapter, maybe?

Note to developers (Benjamin Pierce @bcpierce00)

We should also certainly note it here!

Notice that in Lean's version, the arguments a, b, and c are implicit.

If we simply write apply trans_eq, Lean can infer some arguments from the goal, but not the intermediate list or the hypotheses needed for the lemma's premises.

example (u v w x y z : Nat) (h₁ : [u, v] = [w, x]) (h₂ : [w, x] = [y, z]) : [u, v] = [y, z] := unsolved goals u v w x y z:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [u, v] = ?b u v w x y z:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ ?b = [y, z] u v w x y z:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ List Natu:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [u, v] = [y, z] u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [u, v] = ?bu:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ ?b = [y, z]u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ List Nat

Here is the proof state after apply:

unsolved goals
u v w x y z:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [u, v] = ?b

u v w x y z:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ ?b = [y, z]

u v w x y z:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ List Nat

One way to make progress is to supply the arguments and hypotheses explicitly:

example (u v w x y z : Nat) (h₁ : [u, v] = [w, x]) (h₂ : [w, x] = [y, z]) : [u, v] = [y, z] := u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [u, v] = [y, z] All goals completed! 🐙

Thankfully, Lean allows us to use _s for positional arguments that it can infer.

example (u v w x y z : Nat) (h₁ : [u, v] = [w, x]) (h₂ : [w, x] = [y, z]) : [u, v] = [y, z] := u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [u, v] = [y, z] All goals completed! 🐙

Alternatively, if we know the name of the argument we are supplying (in this case b), we can name it directly and avoid typing any _s. Such named arguments can be used in function applications generally, not just with apply.

Note to developers (Benjamin Pierce @bcpierce00)

Can we explain why using apply would not tell the reader this?

example (u v w x y z : Nat) (h₁ : [u, v] = [w, x]) (h₂ : [w, x] = [y, z]) : [u, v] = [y, z] := u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [u, v] = [y, z] u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [u, v] = [w, x]u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [w, x] = [y, z] u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [w, x] = [y, z] All goals completed! 🐙

By convention, we use exact for situations when we can completely finish the proof with a single application.

example (u v w x y z : Nat) (h₁ : [u, v] = [w, x]) (h₂ : [w, x] = [y, z]) : [u, v] = [y, z] := u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [u, v] = [y, z] All goals completed! 🐙

We can also use calc.

example (u v w x y z : Nat) (h₁ : [u, v] = [w, x]) (h₂ : [w, x] = [y, z]) : [u, v] = [y, z] := u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [u, v] = [y, z] calc [u, v] = [w, x] := u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [u, v] = [w, x] All goals completed! 🐙 _ = [y, z] := u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [w, x] = [y, z] All goals completed! 🐙
Note to developers (Benjamin Pierce @bcpierce00)

The last line is a bit mysterious...

7.3.2. Forward Reasoning with apply🔗

We can also use the apply tactic to rewrite hypotheses.

The ordinary apply tactic is a form of backward reasoning. It says "We are trying to prove a and we know b → a, so if we can prove b we'll be done."

By contrast, the variant apply ... at ... is forward reasoning: it says "We know b and we know b → a, so we also know a."

example (n m p q : Nat) (h : n = m → p = q) (hnm : n = m) : p = q := n:Natm:Natp:Natq:Nath:n = m → p = qhnm:n = m⊢ p = q n:Natm:Natp:Natq:Nath:n = m → p = qhnm:p = q⊢ p = q All goals completed! 🐙

7.4. Specializing Hypotheses🔗

If h is a quantified hypothesis in the current context — i.e., h : ∀ (x : α), P x — then we can use have to obtain a special case of h by supplying a value for x.

example (m : Nat) (h : ∀ n, m * n = 0) : m = 0 := m:Nath:∀ (n : Nat), m * n = 0⊢ m = 0 m:Nath✝:∀ (n : Nat), m * n = 0h:m * 1 = 0⊢ m = 0 m:Nath✝:∀ (n : Nat), m * n = 0h:m = 0⊢ m = 0 All goals completed! 🐙

If we don't care to keep this old hypothesis around, we can use the replace tactic instead.

example (m : Nat) (h : ∀ n, m * n = 0) : m = 0 := m:Nath:∀ (n : Nat), m * n = 0⊢ m = 0 m:Nath:m * 1 = 0⊢ m = 0 m:Nath:m = 0⊢ m = 0 All goals completed! 🐙

Specializing a hypothesis in this way is common enough that Lean provides a separate specialize tactic for it. For example, specialize h 1 is a more concise way of writing replace h := h 1:

example (m : Nat) (h : ∀ n, m * n = 0) : m = 0 := m:Nath:∀ (n : Nat), m * n = 0⊢ m = 0 m:Nath:m * 1 = 0⊢ m = 0 m:Nath:m = 0⊢ m = 0 All goals completed! 🐙

Tactics like have and replace can also be used with lemmas and theorems we've already proven, not just things in the immediate proof context. Using these tactics before apply gives us yet another way to control where apply does its work.

example (u v w x y z : Nat) (h₁ : [u, v] = [w, x]) (h₂ : [w, x] = [y, z]) : [u, v] = [y, z] := u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]⊢ [u, v] = [y, z] u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]h:∀ (a c : List Nat), a = [w, x] → [w, x] = c → a = c⊢ [u, v] = [y, z] u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]h:∀ (a c : List Nat), a = [w, x] → [w, x] = c → a = c⊢ [u, v] = [w, x]u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]h:∀ (a c : List Nat), a = [w, x] → [w, x] = c → a = c⊢ [w, x] = [y, z] /- This tactic closes a goal if it appears anywhere in the context. In this case we could also write `exact h₁` ... -/ u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u, v] = [w, x]h₂:[w, x] = [y, z]h:∀ (a c : List Nat), a = [w, x] → [w, x] = c → a = c⊢ [w, x] = [y, z] /- .. and here we could also write `exact h₂` -/ All goals completed! 🐙
Note to developers (Benjamin Pierce @bcpierce00)

Is this the first place readers are seeing assumption? If so, it should not be buried in a comment in the example.

7.5. Generalizing the Induction Hypothesis🔗

Recall this function for doubling a natural number from the Induction chapter:

def Nat.double (n : Nat) : Nat := match n with | 0 => 0 | n' + 1 => double n' + 2

Suppose we want to show that Nat.double is injective (i.e., it maps different arguments to different results).

theorem double_injective (n m : Nat) (h : n.double = m.double) : n = m := n:Natm:Nath:n.double = m.double⊢ n = m induction n with m:Nath:Nat.double 0 = m.double⊢ 0 = m cases m with h:Nat.double 0 = Nat.double 0⊢ 0 = 0 All goals completed! 🐙 m':Nath:Nat.double 0 = (m' + 1).double⊢ 0 = m' + 1 m':Nath:0 = m'.double + 2⊢ 0 = m' + 1 All goals completed! 🐙 m:Natn':Natih:n'.double = m.double → n' = mh:(n' + 1).double = m.double⊢ n' + 1 = m cases m with n':Natih:n'.double = Nat.double 0 → n' = 0h:(n' + 1).double = Nat.double 0⊢ n' + 1 = 0 n':Natih:n'.double = Nat.double 0 → n' = 0h:n'.double + 2 = 0⊢ n' + 1 = 0 All goals completed! 🐙 n':Natm':Natih:n'.double = (m' + 1).double → n' = m' + 1h:(n' + 1).double = (m' + 1).double⊢ n' + 1 = m' + 1
unsolved goals
n' m':Natih:n'.double = (m' + 1).double → n' = m' + 1h:(n' + 1).double = (m' + 1).double⊢ n' = m'

We get stuck, because the induction hypothesis ih is too specific to be useful.

What went wrong?

Trying to carry out this proof by induction on n with m fixed doesn't work, because we are then trying to prove a statement involving every n but just a particular m.

A successful proof of double_injective needs to generalize m when carrying out the induction on n, so that the induction hypothesis holds for every m, rather than for just the particular m in the context. That is, we want an induction hypothesis like this:

ih : ∀ m, n'.double = m.double → n' = m

We can obtain this generalized induction hypothesis by writing

induction n generalizing m with
theorem double_injective (n m : Nat) (h : n.double = m.double) : n = m := n:Natm:Nath:n.double = m.double⊢ n = m induction n generalizing m with m:Nath:Nat.double 0 = m.double⊢ 0 = m cases m with h:Nat.double 0 = Nat.double 0⊢ 0 = 0 All goals completed! 🐙 m':Nath:Nat.double 0 = (m' + 1).double⊢ 0 = m' + 1 All goals completed! 🐙 n':Natih:∀ (m : Nat), n'.double = m.double → n' = mm:Nath:(n' + 1).double = m.double⊢ n' + 1 = m cases m with n':Natih:∀ (m : Nat), n'.double = m.double → n' = mh:(n' + 1).double = Nat.double 0⊢ n' + 1 = 0 All goals completed! 🐙 n':Natih:∀ (m : Nat), n'.double = m.double → n' = mm':Nath:(n' + 1).double = (m' + 1).double⊢ n' + 1 = m' + 1 n':Natih:∀ (m : Nat), n'.double = m.double → n' = mm':Nath:(n' + 1).double = (m' + 1).double⊢ n' = m' n':Natih:∀ (m : Nat), n'.double = m.double → n' = mm':Nath:(n' + 1).double = (m' + 1).double⊢ n'.double = m'.double -- now works n':Natih:∀ (m : Nat), n'.double = m.double → n' = mm':Nath:n'.double + 2 = m'.double + 2⊢ n'.double = m'.double All goals completed! 🐙

The thing to take away from all this is that you need to be careful, when using induction, that your induction hypothesis is not too specific. When proving a proposition quantified over variables n and m by induction on n, it is sometimes crucial to generalize m, so that the induction hypothesis applies to every m rather than just the particular m in the context.

7.6. Rewriting with Conditional Statements🔗

example (n m p q : Nat) (h : n.double = m.double) (hm : m + p = q) : n + p = q := m✝:Natn✝:Natm':Natn':Natn:Natm:Natp:Natq:Nath:n.double = m.doublehm:m + p = q⊢ n + p = q m✝:Natn✝:Natm':Natn':Natn:Natm:Natp:Natq:Nath:n.double = m.doublehm:m + p = q⊢ m + p = qm✝:Natn✝:Natm':Natn':Natn:Natm:Natp:Natq:Nath:n.double = m.doublehm:m + p = q⊢ n.double = m.double m✝:Natn✝:Natm':Natn':Natn:Natm:Natp:Natq:Nath:n.double = m.doublehm:m + p = q⊢ m + p = q All goals completed! 🐙 m✝:Natn✝:Natm':Natn':Natn:Natm:Natp:Natq:Nath:n.double = m.doublehm:m + p = q⊢ n.double = m.double All goals completed! 🐙

If we rewrite with a conditional statement of the form P → a = b, then Lean tries to rewrite with a = b, and then asks us to prove P in a new subgoal. If the statement has more than one assumption, then we get one subgoal for each assumption.

7.7. Review🔗

Here are the tactics we've seen so far.

Managing goals and hypotheses:

  • intro h: move an assumption/quantified variable from the goal into the local context

  • apply thm: use a theorem, hypothesis, or constructor whose conclusion matches the goal; its premises become new goals

  • apply thm at h: use a theorem on a hypothesis in the context, replacing h by the resulting fact (forward reasoning)

  • specialize h ...: instantiate quantified variables in a hypothesis, modifying h in place

  • replace h := ...: replace a hypothesis with a newly proved fact

  • have h : P := ...: prove a local fact P and add it to the context with the name h

  • contradiction: close the current goal when the context contains contradictory assumptions

Equality, rewriting, and unfolding:

  • rfl: close an equality that holds by reflexivity (possibly after computation)

  • rw [h]: rewrite the goal using an equality hypothesis or theorem

  • rw [d]: unfold a definition in the goal

  • rw [h] at h': rewrite a hypothesis using an equality hypothesis or theorem

  • rw [d] at h': unfold a definition in a hypothesis

  • symm: reverse an equality goal, changing t = u to u = t

  • symm at h: reverse an equality hypothesis

  • calc: prove a goal about equality or another transitive relation by giving a sequence of intermediate steps

  • congr: use congruence to reduce an equality between expressions with the same outer form; for example, a goal f x = f y may be reduced to x = y

  • injection h with ...: use injectivity of constructors to extract equalities from equations between constructor applications

  • injections: repeatedly use constructor injectivity on suitable equalities in the context

Case analysis:

  • cases x: reason separately about the possible constructors of an inductively defined value

  • cases h : e: perform case analysis on an expression e and add an equation named h recording the result of the case analysis

Induction:

  • induction x: prove the goal by induction on an inductively defined value

  • induction x generalizing y: induction on x while generalizing the listed local variables, giving a more general induction hypothesis

7.8. Additional Exercises🔗

Source revision: 9cc9a7b, committed 2026-09-28 19:09 UTC