4. UsingLean: Using the Full Power of a Proof Assistant🔗
In this chapter, we will learn to write more idiomatic Lean using its more
powerful tools.
This includes the natural numbers from its standard library,
tactics which can search for lemmas from the standard library, namespaces for
organizing lemmas, and a new tactic, calc,
which enables more readable and concise proofs.
Until now, we have been working with our own custom natural numbers, using the
Nat type that we defined in Basics.
As you might have guessed, Lean has a built-in type of natural numbers, also called
Nat, which is automatically imported into .lean files by default. Its definition
is essentially the same as our custom Nat, but it comes with a large library of useful theorems.
Programmers and mathematicians usually apply these automatically rather than by writing out
rewrite steps by hand. For example, here is a simple proof of equality using our
custom Nats.
We made Lean enforce this pedagogical style using attribute [irreducible]
on definitions like mul
and add. This forced us to write proofs using tactics like rw
rather than simplifying definitions.
This approach is useful in a textbook for understanding the structure of
natural numbers and for providing early practice with writing proofs. But it
is also tedious in the long term.
Instead of doing this, programmers and mathematicians use the built-in
Nat and the powerful features of Lean to automatically prove
properties about natural numbers and to compute with them.
endOldNats-- Now, we are using Lean's built-in natural numbers.example:(2*2:Nat)=4:=byn:Nat⊢ 2*2=4rflAll goals completed! 🐙
The annotation : Nat tells Lean that we are using its built-in Nat type.
Definitions in the built-in Nat library are not marked @[irreducible], so we can
perform automatic simplification of functions on natural numbers,
which is appropriate when their low-level behaviors are not the primary focus of proofs.
Doing so is very helpful for large numbers — we would not want to write out
the hundreds or thousands of rewrite steps needed for proving examples like the
following!
Of course, rfl still can't close goals where the values of the terms are unknown.
example(nm:Nat)(h:n=m):n=m:=byn✝:Natn:Natm:Nath:n=m⊢ n=m-- `rfl` will not work here!-- First rewrite the goal with `h`; then the two sides are identical.rw[hn✝:Natn:Natm:Nath:n=m⊢ m=m]All goals completed! 🐙
From now on we will use the built-in Nat type.
We will write Nat.<theorem> to reference Lean's version
of <theorem>; by convention, theorems about a type live in the namespace of
that type.
Because we
did not write or prove theorems for built-in Nats ourselves, we may not
know (or remember) all the available theorems.
Lean provides a few ways to search through the standard library to find theorems
that may be useful during a particular proof. The first way is the exact?
tactic. This tactic searches the standard library for a theorem that can be applied,
along with the hypotheses in the context, to exactly close the current goal.
If you are using the Lean extension in VS Code, the InfoView will
have a blue [apply] button that shows the suggested theorem to
close the goal. Alternatively, VS Code may show an inline suggestion
(lightbulb) button above the exact?. You can click either of
these buttons to replace the occurrence of exact? with the tactic
it found to complete the proof; idiomatic Lean should not contain
exact? tactics (or any other ? tactics) in the finished
versions of proofs.
The exact? tactic is useful when we just need a single library theorem to get us over
the finish line, but it is not so helpful when we are deep in the middle of a proof
or wondering how to get started on one. Fortunately, there are other tactics
that can help.
The rw? tactic searches for any theorems
that you could use to rewrite (rather than complete) the current goal.
example(nm:Nat):n+m=m+n:=byn✝:Natn:Natm:Nat⊢ n+m=m+nTry this:[apply]rw [Nat.add_comm]-- no goalsrw?All goals completed! 🐙
Try this:[apply]rw [Nat.add_comm]-- no goals
However, unlike exact?, just because rw? suggests
a theorem to you does not automatically imply that it will be useful.
In the example below, many of the theorems rw? suggests
will not progress towards completing the proof; you will need to
carefully look through its suggestions to see which ones seem useful.
example(nmk:Nat):(n+m)+k=m+(n+k):=unsolved goalsn✝nmk:Nat⊢ n+k+m=m+(n+k)byn✝:Natn:Natm:Natk:Nat⊢ n+m+k=m+(n+k)-- lots of suggestions to look through here!Try this:[apply]rw [Nat.add_right_comm]-- n+k+m=m+(n+k)Try this:[apply]rw [Nat.add_assoc]-- n+(m+k)=m+(n+k)Try this:[apply]rw [Nat.add_left_comm]-- n+m+k=n+(m+k)Try this:[apply]rw [Nat.add_comm]-- k+(n+m)=m+(n+k)Try this:[apply]rw [← Nat.add_assoc]-- n+m+k=m+n+kTry this:[apply]rw [Nat.eq_iff_testBit_eq]-- ∀(i:Nat),(n+m+k).testBiti=(m+(n+k)).testBitifound an applicable rewrite lemma, but the corresponding tactic failed:(expose_names; rw [propext(Nat.ext_div_mod_iffn_1(n+m+k)(m+(n+k)))])-- (n+m+k)/n_1=(m+(n+k))/n_1∧(n+m+k)%n_1=(m+(n+k))%n_1It may be possible to correct this proof by adding type annotations, explicitly specifying implicit arguments, or eliminating unnecessary function abstractions.Try this:[apply]rw [Nat.le_antisymm_iff]-- n+m+k≤m+(n+k)∧m+(n+k)≤n+m+kTry this:[apply]rw [← Std.PRange.Nat.succMany_eq]-- Std.PRange.succManyk(n+m)=m+(n+k)found an applicable rewrite lemma, but the corresponding tactic failed:(expose_names; rw [← BitVec.cpopNatRec_allOnesNat.le.refl])-- (BitVec.allOnesk).cpopNatReck(n+m)=m+(n+k)It may be possible to correct this proof by adding type annotations, explicitly specifying implicit arguments, or eliminating unnecessary function abstractions.found an applicable rewrite lemma, but the corresponding tactic failed:(expose_names; rw [← Nat.add_sub_sub_cancelNat.le.refl])-- k+(n+m)-(k-k)=m+(n+k)It may be possible to correct this proof by adding type annotations, explicitly specifying implicit arguments, or eliminating unnecessary function abstractions.Try this:[apply]rw [← Nat.add_left_max_self]-- max(n+m+k)k=m+(n+k)Try this:[apply]rw [← Nat.add_right_max_self]-- max(n+m+k)(n+m)=m+(n+k)Try this:[apply]rw [← Nat.succ_add_sub_one]-- (n+m).succ+k-1=m+(n+k)Try this:[apply]rw [← Nat.max_add_left_self]-- maxk(n+m+k)=m+(n+k)Try this:[apply]rw [← Nat.max_add_right_self]-- max(n+m)(n+m+k)=m+(n+k)Try this:[apply]rw [← Nat.add_succ_sub_one]-- n+m+k.succ-1=m+(n+k)Try this:[apply]rw [← Nat.add_eq]-- (n+m).addk=m+(n+k)Try this:[apply]rw [← Rat.ofNat_eq_ofNat]-- OfNat.ofNat(n+m+k)=OfNat.ofNat(m+(n+k))Try this:[apply]rw [← Nat.compare_eq_eq]-- compare(n+m+k)(m+(n+k))=Ordering.eqrw?n✝:Natn:Natm:Natk:Nat⊢ n+k+m=m+(n+k)
We strongly recommend against blindly using rw? and
accepting its suggestions without due consideration! You will find
this to be a slow and frustrating way to write proofs. Instead, we
suggest figuring out what you would like your next step to be,
conceptually, and then using rw? to search for a theorem
that implements it. If no such theorem exists, you may need to prove it
yourself.
Exercise★(mul_three_beq)
Prove the following theorems about Nats.
You should not need induction;
find the theorems you need using rw? and exact?.
In Lean proofs, long rw chains are useful, but they are sometimes
hard to read because the intermediate goals are invisible.
Furthermore, sometimes we know exactly how we want to manipulate the terms of a proof, but
don't want to have the tactics like Nat.add_comm and
Nat.add_assoc "guess" which subterms to rewrite.
The calc tactic writes down the intermediate goals of a proof, and
allows us to specify exactly which rewrite rules to apply at each step. It is designed
to mimic the style of proofs in mathematics textbooks, which will often look something like this:
n + (m + k)
= (n + m) + k ... [by associativity of addition]
= (m + n) + k ... [by commutativity of addition]
= m + (n + k) ... [by associativity of addition]
Note how we can see each intermediate step of this proof when we
look at it this way. Let's look at how we might prove this theorem
(i.e., that n + (m + k) = m + (n + k)) in Lean.
Now, the same theorem written with calc.
Note how each intermediate goal is visible in the source.
example(nmk:Nat):n+(m+k)=m+(n+k):=byn✝:Natn:Natm:Natk:Nat⊢ n+(m+k)=m+(n+k)calcn+(m+k)/- one side of the goal is the argument to `calc`...
... and each subsequent line is a transformation, with a tactic. -/n+(m+k)=(n+m)+k:=byn✝:Natn:Natm:Natk:Nat⊢ n+(m+k)=n+m+krw[Nat.add_assocn✝:Natn:Natm:Natk:Nat⊢ n+(m+k)=n+(m+k)]All goals completed! 🐙(n+m)+k=(m+n)+k:=byn✝:Natn:Natm:Natk:Nat⊢ n+m+k=m+n+krw[Nat.add_commnmn✝:Natn:Natm:Natk:Nat⊢ m+n+k=m+n+k]All goals completed! 🐙/- once a line matches the other side of the equality in the main goal
(in this case `m + (n + k)`), the calc tactic succeeds. -/(m+n)+k=m+(n+k):=byn✝:Natn:Natm:Natk:Nat⊢ m+n+k=m+(n+k)rw[Nat.add_assocn✝:Natn:Natm:Natk:Nat⊢ m+(n+k)=m+(n+k)]All goals completed! 🐙
We can also write the proof like this to be a bit more concise:
Whereas before, the left-hand side of each equality in the
calc tactic was repeated from the right-hand side of the
previous one, we can replace the left-hand side entirely with an _.
Now our Lean proof looks quite a bit like the textbook one we saw earlier!
Tactic `rewrite` failed: Did not find an occurrence of the pattern?n+?m+?kin the target expressionaddThricen=n+addTwicenn✝n:Nat⊢ addThricen=n+addTwicen
The reason is that the expression in which we are trying to rewrite
Nat.add_assoc isn't of the form n + m + k precisely; it is addThricen.
We need to unfold the underlying definitions of
addThrice and addTwice so that rw, which only operates on syntax,
can see the addition.
We can do this using the rw tactic.
example(n:Nat):addThricen=n+addTwicen:=byn✝:Natn:Nat⊢ addThricen=n+addTwicen-- `rw [addThrice]` unfolds `addThrice`, replacing it with its definitionrw[addThricen✝:Natn:Nat⊢ n+n+n=n+addTwicen]n✝:Natn:Nat⊢ n+n+n=n+addTwicen-- this likewise unfolds `addTwice`rw[addTwicen✝:Natn:Nat⊢ n+n+n=n+(n+n)]n✝:Natn:Nat⊢ n+n+n=n+(n+n)-- Now, proving our goal only requires associativity of additionsrw[Nat.add_assocn✝:Natn:Nat⊢ n+(n+n)=n+(n+n)]All goals completed! 🐙
Since Lean does not unfold most definitions automatically, we use tactics
like rw to do so selectively, in goals and hypotheses, in order to guide
how a proof is carried out.
Exercise★(rwUnfold)
Complete this proof, using rw to unfold the definition of addThrice as appropriate.
example(nm:Nat)(h:2*n=m*2):n+n=m+m:=byn✝:Natn:Natm:Nath:2*n=m*2⊢ n+n=m+m-- use rw? to construct the proofrw[Nat.mul_comm,n✝:Natn:Natm:Nath:n*2=m*2⊢ n+n=m+mNat.mul_two,n✝:Natn:Natm:Nath:n+n=m*2⊢ n+n=m+mNat.mul_twon✝:Natn:Natm:Nath:n+n=m+m⊢ n+n=m+m]athn✝:Natn:Natm:Nath:n+n=m+m⊢ n+n=m+mexacthAll goals completed! 🐙
With the ability to unfold definitions via rewriting, one may wonder why we need
simplification rules like add_zero and add_succ. As mentioned when motivating these
rules, they provide some engineering benefits: The rules tend to stay the same even
as definitions change, which helps avoid proof breakages. Avoiding such breakages
is particularly important with proofs using parts of Lean's standard library,
which are often implemented in ways that are very efficient but less friendly to proofs.
Sometimes when you unfold a definition your hypothesis or goal may become hard to understand.
When that happens, it can be useful to simplify it.
To apply simplifications similar to those that rfl does,
but without also trying to close an equality goal,
you can use the tactic dsimp only or dsimp only at h.
example:(funx=>x+0)n=n:=byn:Nat⊢ (funx=>x+0)n=ndsimponlyn:Nat⊢ n+0=n-- applies the function to its argumentrw[Nat.add_zeron:Nat⊢ n=n]All goals completed! 🐙
If we did not simplify here before attempting to rewrite, we would get an error:
example:(funx=>x+0)n=n:=byn:Nat⊢ (funx=>x+0)n=nrw[Tactic `rewrite` failed: Did not find an occurrence of the pattern?n+0in the target expression(funx=>x+0)n=nn:Nat⊢ (funx=>x+0)n=nNat.add_zeron:Nat⊢ (funx=>x+0)n=n]n:Nat⊢ (funx=>x+0)n=n
Tactic `rewrite` failed: Did not find an occurrence of the pattern?n+0in the target expression(funx=>x+0)n=nn:Nat⊢ (funx=>x+0)n=n
We have previously seen the same issue with lemmas like add_zero, leading to situations
in which we have to rewrite multiple times in a row.
To make this situation a bit better, we can use the repeat tactic combinator,
which takes a tactic as its argument and repeats it as many times as it can.
The repeat tactic is a simple source of proof
automation in Lean, as is the use of simplification via dsimp
and rfl. Lean's full tactic library, and tactic-writing
metaprogramming language, offer much more.
The Automation chapter will
introduce the powerful, and commonly used, automated tactic simp,
which can sometimes solve complex goals by itself.
We'll also talk about other tactic combinators like repeat.
But, using these tools now does not help (in fact, it hurts!) the
process of learning logical reasoning, formal theorem proving, and
Lean. Additionally, real Lean programmers are careful when using
automation: it can hurt the readability of a proof, and real-world
Lean is often used to communicate a result as much as to prove
it. We will continue to use only simple tactics and rw,
for most of this volume so that you have a firm
grasp of both the logic behind the proofs you are writing and the
ways to structure those proofs to make your logic clear.
Now that we've switched to using Lean's standard library, we can
redefine some of the functions from the last few chapters on Nats.
Note that, for the built-in Nat type, the patterns 0 and
n+1 correspond to Nat.zero and Nat.succn.
Likewise, the pattern n+2 is equivalent to n+1+1.
Prove some of these theorems using the techniques we've discussed this chapter.
Note that we defined these functions in the Nat namespace;
Lean's naming conventions advise that functions on a type should be defined in that type's
namespace in almost all circumstances.
When we define functions this way,
something interesting happens to the way Lean's InfoView prints them. Take a look at
the InfoView inside the proof of this theorem before the rfl tactic:
Instead of printing the goal the way we wrote it in the theorem statement, Lean
prints (n+3).even=(n+1).even! This is an example of Lean's field notation,
whereby Lean prints functions inside the namespace of a type after their first argument,
separated by a .. At first glance, this may appear similar to how object-oriented methods work,
but it's really just a syntactic variation on the normal function-application style
we've seen so far. That is, Nat.evenn and n.even are just different ways to write
the exact same term.
In previous chapters we disabled this notation by putting set_option pp.fieldNotation false
at the top of each file, but from now on we will leave it enabled, since field notation
is recommended in idiomatic Lean developments.
As an example, observe the difference in how Lean prints the goal in the following two examples:
One inconvenient aspect of our definition of even n is the
recursive call on n' when n = n' + 2. This makes proofs about even n
harder when done by induction on n, since we may need an
induction hypothesis about n' + 2, while induction just gives us one about n' + 1. The following lemma proves even (n + 1) flips the parity, which gives an
alternative characterization that works better with induction. We'll see uses of
this theorem in Lists.
In the remainder of the book, we use Lean's built-in natural numbers everywhere.
We also recommend using rw? and exact? to search for lemmas
(though these should not appear in finished proofs).
With these tools in hand, we
can begin to prove properties about more sophisticated forms of data, beginning with
Lists.
Source revision: e85fe77, committed 2026-10-06 21:16 UTC