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.
We can prove the injectivity of Nat.succ by using the Nat.pred function:
example(nm:Nat)(h:n+1=m+1):n=m:=n:Natm:Nath:n+1=m+1⊢ n=mn: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=mrw[Nat.pred_succn:Natm:Nath:n+1=m+1this:n=(n+1).pred⊢ m=m]All goals completed! 🐙
As a convenience, the injection tactic allows us to
exploit injectivity of any constructor (not just Nat.succ).
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).
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.
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:
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:
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:=byn:Nath:1+n=0⊢ 2+2=5Tactic `contradiction` failedn:Nath:1+n=0⊢ 2+2=5contradictionn: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.
Tactic `injection` failed: equality of constructor applications expectedxy: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.
example(abcd:Nat)(h1:a=b+1)(h2:d=c+1):(a+c,true)=(b+d,true):=bya: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 -/congr1e_fsta:Natb:Natc:Natd:Nath1:a=b+1h2:d=c+1⊢ a+c=b+drw[h1,e_fsta:Natb:Natc:Natd:Nath1:a=b+1h2:d=c+1⊢ b+1+c=b+dh2,e_fsta:Natb:Natc:Natd:Nath1:a=b+1h2:d=c+1⊢ b+1+c=b+(c+1)Nat.add_assoc,e_fsta:Natb:Natc:Natd:Nath1:a=b+1h2:d=c+1⊢ b+(1+c)=b+(c+1)Nat.add_comm1ce_fsta:Natb:Natc:Natd:Nath1:a=b+1h2:d=c+1⊢ b+(c+1)=b+(c+1)]All goals completed! 🐙
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:
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.
Adding the h : ⋯ qualifier saves this information so we can use it.
theoremkeepIf_some{α:Type}(test:α→Bool)(xy:α)(h:keepIftestx=somey):x=y:=byα:Typetest:α→Boolx:αy:αh:keepIftestx=somey⊢ x=yrw[keepIfα:Typetest:α→Boolx:αy:αh:(biftestxthensomexelsenone)=somey⊢ x=y]athα:Typetest:α→Boolx:αy:αh:(biftestxthensomexelsenone)=somey⊢ 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.caseshTest:testxwith|false=>falseα:Typetest:α→Boolx:αy:αh:(biftestxthensomexelsenone)=someyhTest:testx=false⊢ x=yrw[hTestfalseα:Typetest:α→Boolx:αy:αh:(biffalsethensomexelsenone)=someyhTest:testx=false⊢ x=y]athfalseα:Typetest:α→Boolx:αy:αh:(biffalsethensomexelsenone)=someyhTest:testx=false⊢ x=ycontradictionAll goals completed! 🐙|true=>trueα:Typetest:α→Boolx:αy:αh:(biftestxthensomexelsenone)=someyhTest:testx=true⊢ x=yrw[hTesttrueα:Typetest:α→Boolx:αy:αh:(biftruethensomexelsenone)=someyhTest:testx=true⊢ x=y]athtrueα:Typetest:α→Boolx:αy:αh:(biftruethensomexelsenone)=someyhTest:testx=true⊢ x=yinjectionsAll goals completed! 🐙
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.
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:
The goal must match the hypothesis for apply to
work:
example(nm:Nat)(h:n=0→n=m)(hn:n=0):m=n:=byn: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. -/symmn:Natm:Nath:n=0→n=mhn:n=0⊢ n=mapplyhn:Natm:Nath:n=0→n=mhn:n=0⊢ n=0exacthnAll 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.
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.
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?
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.
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:
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(uvwxyz:Nat)(h₁:[u,v]=[w,x])(h₂:[w,x]=[y,z]):[u,v]=[y,z]:=byu:Natv:Natw:Natx:Naty:Natz:Nath₁:[u,v]=[w,x]h₂:[w,x]=[y,z]⊢ [u,v]=[y,z]haveh:=trans_eq(b:=[w,x])u:Natv:Natw:Natx:Naty:Natz:Nath₁:[u,v]=[w,x]h₂:[w,x]=[y,z]h:∀(ac:ListNat),a=[w,x]→[w,x]=c→a=c⊢ [u,v]=[y,z]applyhau:Natv:Natw:Natx:Naty:Natz:Nath₁:[u,v]=[w,x]h₂:[w,x]=[y,z]h:∀(ac:ListNat),a=[w,x]→[w,x]=c→a=c⊢ [u,v]=[w,x]au:Natv:Natw:Natx:Naty:Natz:Nath₁:[u,v]=[w,x]h₂:[w,x]=[y,z]h:∀(ac:ListNat),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₁` ... -/assumptionau:Natv:Natw:Natx:Naty:Natz:Nath₁:[u,v]=[w,x]h₂:[w,x]=[y,z]h:∀(ac:ListNat),a=[w,x]→[w,x]=c→a=c⊢ [w,x]=[y,z]/- .. and here we could also write `exact h₂` -/assumptionAll 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.
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 everyn but just a particularm.
A successful proof of double_injective needs to generalizem 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
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 generalizem, so that the induction hypothesis applies to every m
rather than just the particular m in the context.
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.