LeanChapter 02
Tactics
Goal states, common proof tactics, error messages, and tools for finding Mathlib lemmas.
Contents
Chapter 1 represented proofs directly as terms. Longer arguments are usually
written as tactic scripts: sequences of commands that construct proof terms.
The keyword by begins a tactic script.
Tactics operate on the goal state, which lists the current hypotheses and the conclusion still to prove. Each tactic transforms this state or closes a goal. In the playground, placing the cursor after a tactic displays the state at that point.
y1 y0 : Fin 3 → ℝ
h : ∀ i, y0 i ≤ y1 i
⊢ y0 0 ≤ y1 0
The hypotheses appear above the turnstile ⊢; the target appears below it. A
proof is complete when no goals remain.
intro and exact
theorem effect_nonneg_at_zero (y1 y0 : Fin 3 → ℝ) :
(∀ i, y0 i ≤ y1 i) → y0 0 ≤ y1 0 := by
intro h
exact h 0intro h moves the antecedent of an →, or the bound variable of a ∀, out
of the goal and into the hypotheses. It is the tactic spelling of fun h => ….
exact e closes the goal with the term e, which must have exactly the goal’s
type. The proof above is one of each, starting from
y1 y0 : Fin 3 → ℝ
⊢ (∀ (i : Fin 3), y0 i ≤ y1 i) → y0 0 ≤ y1 0
intro hThe target is an implication, so its antecedent can be assumed:
intro hnames ithand removes it from the target. What is left to prove is the conclusion alone, the inequality at unit0.Goaly1 y0 : Fin 3 → ℝ h : ∀ (i : Fin 3), y0 i ≤ y1 i ⊢ y0 0 ≤ y1 0exact h 0his a proof of a∀, which chapter 1 established is a function, soh 0is its instance at unit0— a proof ofy0 0 ≤ y1 0. That is the goal character for character, soexactaccepts it and no goals remain.
That pair is the floor. Everything else is a way of getting a goal into a shape
where exact applies.
Working backwards: apply and refine
theorem obs_le_of_le (a b c : ℝ) (hab : a ≤ b) (hbc : b ≤ c) : a ≤ c := by
apply le_trans
· exact hab
· exact hbc
theorem mean_bounds (a b : ℝ) (ha : 0 ≤ a) (hb : 0 ≤ b) : 0 ≤ a ∧ 0 ≤ a + b := by
refine ⟨ha, ?_⟩
exact add_nonneg ha hbapply f unifies the conclusion of f with the goal and leaves you owing
f’s hypotheses. With le_trans : a ≤ b → b ≤ c → a ≤ c and the goal
a ≤ c, one goal becomes two, and the intermediate b is a metavariable that
later steps will pin down:
case a
a b c : ℝ
hab : a ≤ b
hbc : b ≤ c
⊢ a ≤ ?b
case a
a b c : ℝ
hab : a ≤ b
hbc : b ≤ c
⊢ ?b ≤ c
case b
a b c : ℝ
hab : a ≤ b
hbc : b ≤ c
⊢ ℝ
Three goals, not two, and the third is the surprise: ?b is an unknown real
number, so Lean owes itself a term of type ℝ as well as the two
inequalities. You never have to supply it — exact hab forces ?b := b by
unification and the third goal disappears with it — but seeing it once explains
why apply sometimes leaves a goal that is a type rather than a proposition.
When that is a nuisance, exact le_trans hab hbc or
refine le_trans ?_ hbc pins the intermediate down from the start.
The · bullets in the script focus one goal at a time. They are not decoration:
they scope the tactics inside them, so a proof stays readable and a change to
one branch cannot silently leak into the other.
apply le_transle_trans : a ≤ b → b ≤ c → a ≤ c. Its conclusion unifies with the goala ≤ c— fixing the lemma’saandc, and leaving itsbopen — so what remains is the lemma’s two hypotheses, plus the metavariable?bdisplayed above.· exact habThe bullet focuses the first of those goals,
a ≤ ?b. Supplyinghab : a ≤ bcloses it and, by unification, decides?b := b; the goal that asked for a real number is answered by the same act, which is why the script never mentions it.· exact hbcWith
?bnow known to beb, the second goal readsb ≤ c, which ishbc. Reading the script backwards: we claimed the conclusion follows by transitivity, and then paid the two debts transitivity incurs.
refine is exact with holes. You write the shape of the answer and put ?_
where you want a new goal, which is how the second theorem gets from a
conjunction to the one inequality it has to work for.
refine ⟨ha, ?_⟩The goal
0 ≤ a ∧ 0 ≤ a + bis a pair, so a proof of it is a pair. The left component ishaoutright;?_in the right slot says “leave that one to me”, and it becomes the goal shown below.Goala b : ℝ ha : 0 ≤ a hb : 0 ≤ b ⊢ 0 ≤ a + bexact add_nonneg ha hbadd_nonneg : 0 ≤ a → 0 ≤ b → 0 ≤ a + bis the fact that a sum of non-negatives is non-negative. Fed the two hypotheses in order, it has exactly the type of the remaining goal.
This is the tactic to reach for when you know the structure of the argument and not yet the details, which — in a formalization of something you already understand — is most of the time.
Rewriting
theorem obs_of_treated (y1 yobs : ℝ) (h : yobs = y1) (hy : y1 = 3) : yobs = 3 := by
rw [h]
exact hy
theorem obs_of_treated' (y1 yobs : ℝ) (h : yobs = y1) (hy : y1 = 3) : yobs = 3 := by
rw [← hy]
exact h
theorem effect_at_unit (y1 y0 : Fin 3 → ℝ) (c : ℝ)
(h : ∀ i, y1 i - y0 i = c) (hc : c = 2) : y1 1 - y0 1 = 2 := by
have h1 := h 1
rw [hc] at h1
exact h1rw [h] where h : a = b replaces every occurrence of a in the goal by b.
It is the workhorse: a statistical identity is a chain of substitutions, and
rw is substitution.
obs_of_treated is two lines: turn the observed outcome into the treated
potential outcome, then quote the value of that.
rw [h]h : yobs = y1reads left to right, so everyyobsin the goalyobs = 3becomesy1. Statistically this is the substitution “the unit was treated, so what we observed is its treated potential outcome”.Goaly1 yobs : ℝ h : yobs = y1 hy : y1 = 3 ⊢ y1 = 3exact hyThe goal is now
y1 = 3, which is the hypothesishy.
Three features of rw are used throughout the book.
It goes left to right, and ← reverses it. obs_of_treated' proves the
same theorem from the other end.
rw [← hy]hy : y1 = 3used forwards would look for ay1in the goalyobs = 3and find none. The arrow reverses it, so the3in the goal becomesy1instead and the goal turns into the hypothesish.Goaly1 yobs : ℝ h : yobs = y1 hy : y1 = 3 ⊢ yobs = y1exact hh : yobs = y1is now the goal exactly. Choosing the direction is most of the skill; a rewrite that “does not apply” is usually the right lemma facing the wrong way.
It rewrites the goal unless you say otherwise. rw [hc] at h1 rewrites
inside the hypothesis h1. effect_at_unit uses both that and a have to
work on the assumption rather than on the target.
have h1 := h 1his the constant-effects assumption, one equation per unit;h 1is its instance at unit1, andhavegives that instance a name. Note the form:have name := termwith no stated type, because the type is whatever the term has — herey1 1 - y0 1 = c.Goaly1 y0 : Fin 3 → ℝ c : ℝ h : ∀ (i : Fin 3), y1 i - y0 i = c hc : c = 2 h1 : y1 1 - y0 1 = c ⊢ y1 1 - y0 1 = 2rw [hc] at h1hc : c = 2rewritten inh1replaces the constant by its value, soh1becomesy1 1 - y0 1 = 2. Nothing has happened to the goal; the point ofatis that the hypothesis, not the target, was the thing in the wrong shape.exact h1h1is now literally the goal. Two lines of massaging a hypothesis, one line to hand it over — a shape you will write hundreds of times.
It closes the goal if the result is rfl. After a successful rewrite Lean
tries rfl, which is why many rw chains end without an explicit exact. This
is convenient and occasionally confusing: a rw that “does too much” has
usually just finished.
simp and simp only
theorem sum_two_outcomes (y : Fin 3 → ℝ) : y 0 + 0 + (y 1 * 1) = y 0 + y 1 := by
simp only [add_zero, mul_one]
theorem cleanup_hypothesis (x : ℝ) (h : x + 0 = 5) : x = 5 := by
simp only [add_zero] at h
exact hsimp rewrites with the entire @[simp] database until nothing changes. It is
enormously useful and completely opaque: you cannot tell from the script what
was used, and a Mathlib update can change the answer.
simp only [add_zero, mul_one] uses exactly the lemmas named. In anything
someone else will read — a tutorial, a paper artefact, a library — prefer
simp only. The way to get the list is to write simp?, read the
simp only [...] it prints, and paste that in. simp at h normalizes a
hypothesis instead of the goal, and simp_all does both everywhere, which is
powerful and the least readable option of all.
Both theorems in the block are one simp only, and the list is short enough to
read lemma by lemma.
simp only [add_zero, mul_one]In
sum_two_outcomesthe two named lemmas do one job each.add_zero(a + 0 = a) deletes the+ 0, leaving the goal shown below;mul_one(a * 1 = a) then deletes the* 1, after which the two sides are the same term andsimp onlycloses the goal itself. Neither lemma knows anything about outcomes — this is bookkeeping, and the reason to name the lemmas is so that a reader can see that nothing but bookkeeping happened.Goaly : Fin 3 → ℝ ⊢ y 0 + y 1 * 1 = y 0 + y 1simp only [add_zero] at hcleanup_hypothesishas the clutter in the hypothesis instead, soat hpoints the same rewrite there:h : x + 0 = 5becomesh : x = 5. The goal is untouched.Goalx : ℝ h : x = 5 ⊢ x = 5exact hWhich is now the goal. Compare this with
rw [add_zero] at h: the same effect here, butsimp onlykeeps going until nothing in its list applies, so it survives a hypothesis with three stray+ 0s whererwwould need three rewrites.
Building and taking apart
theorem both_nonneg (a b : ℝ) (ha : 0 ≤ a) (hb : 0 ≤ b) : 0 ≤ a ∧ 0 ≤ b := by
constructor
· exact ha
· exact hb
theorem exists_positive_effect (y1 y0 : Fin 3 → ℝ) (h : y0 2 < y1 2) :
∃ i, y0 i < y1 i := by
use 2constructor applies the constructor of an inductive goal. On p ∧ q it
splits one goal into two:
case left
a b : ℝ
ha : 0 ≤ a
hb : 0 ≤ b
⊢ 0 ≤ a
case right
a b : ℝ
ha : 0 ≤ a
hb : 0 ≤ b
⊢ 0 ≤ b
constructorThe goal is a conjunction, and
Andhas one constructor taking one proof per side, soconstructorapplies it and asks for both. The two goals above are the arguments it now needs.· exact haThe first bullet answers
0 ≤ a, which is the hypothesisha.· exact hbThe second answers
0 ≤ bwithhb. This proof isrefine ⟨ha, hb⟩written in three lines instead of one;constructorearns its keep when the two halves are long enough that you want them separated.
use w supplies the witness for an ∃ goal and leaves the property. It also
tries rfl and the hypotheses afterwards, which is why use 2 finishes
exists_positive_effect outright: after substituting the witness the remaining
goal y0 2 < y1 2 is literally the hypothesis h. When use does not close
the goal, you are left with the property to prove, which is what you wanted.
theorem exists_of_exists (y1 y0 : Fin 3 → ℝ) (h : ∃ i, y1 i - y0 i = 2) :
∃ i, 0 < y1 i - y0 i := by
obtain ⟨i, hi⟩ := h
exact ⟨i, by rw [hi]; norm_num⟩
theorem obs_nonneg (yobs y1 y0 : ℝ) (h1 : 0 ≤ y1) (h0 : 0 ≤ y0)
(h : yobs = y1 ∨ yobs = y0) : 0 ≤ yobs := by
rcases h with h | h
· rw [h]; exact h1
· rw [h]; exact h0Going the other way, obtain ⟨i, hi⟩ := h destructs an ∃ or an ∧ in a
hypothesis:
y1 y0 : Fin 3 → ℝ
i : Fin 3
hi : y1 i - y0 i = 2
⊢ ∃ i, 0 < y1 i - y0 i
Note what the witness i is and is not: it is a genuine unit of the
population, opaque except for hi. The rest of the proof has to produce a
new existential out of it.
obtain ⟨i, hi⟩ := hDestruct the hypothesis.
iis the unit the hypothesis promised andhiits property, and the hypothesishitself is gone — its content is now in those two names. The goal, shown above, is unchanged.exact ⟨i, …⟩The goal is an
∃, so the anonymous constructor from chapter 1 builds it: the same unitias the witness, and then a proof that it works.Goaly1 y0 : Fin 3 → ℝ i : Fin 3 hi : y1 i - y0 i = 2 ⊢ 0 < y1 i - y0 irw [hi]Inside the bracket, tactic mode again.
hisays the effect atiequals2, so rewriting turns the goal0 < y1 i - y0 iinto arithmetic on numerals.Goaly1 y0 : Fin 3 → ℝ i : Fin 3 hi : y1 i - y0 i = 2 ⊢ 0 < 2norm_num0 < 2, whichnorm_numdecides. The whole proof is “the unit that has effect exactly two is a unit that has positive effect”, and each of the four steps is one clause of that sentence.
rcases h with ⟨i, hi⟩ is the same tactic spelled differently, and
cases h with | intro i hi => … is the low-level version. Use obtain for
conjunctions and existentials, and rcases h with h | h for a disjunction,
where the | produces one goal per side:
case inl
yobs y1 y0 : ℝ
h1 : 0 ≤ y1
h0 : 0 ≤ y0
h : yobs = y1
⊢ 0 ≤ yobs
case inr
yobs y1 y0 : ℝ
h1 : 0 ≤ y1
h0 : 0 ≤ y0
h : yobs = y0
⊢ 0 ≤ yobs
rcases h with h | hOne goal per disjunct, each with the corresponding equation available under the same name
h— reusing the name is idiomatic when the two branches are symmetric. The two states above are whatrcasesproduced.· rw [h]; exact h1In the first branch
h : yobs = y1, so rewriting the goal0 ≤ yobsgives the goal shown below, which is the hypothesish1. The;is sequencing inside one bullet, nothing more.Goalyobs y1 y0 : ℝ h1 : 0 ≤ y1 h0 : 0 ≤ y0 h : yobs = y1 ⊢ 0 ≤ y1· rw [h]; exact h0The second branch is the same two tactics with
y0in place ofy1. This is the whole of design-based reasoning about an observed outcome: it is one of the two potential outcomes, you do not know which, so you argue twice.
That two-case shape is the treated/control split, and you will meet it in every argument about an indicator.
Structuring a proof: have, show, calc
theorem variance_shortcut (x m : ℝ) : (x - m) ^ 2 = x ^ 2 - 2 * m * x + m ^ 2 := by
have expand : (x - m) ^ 2 = (x - m) * (x - m) := sq (x - m) ▸ rfl
show (x - m) ^ 2 = x ^ 2 - 2 * m * x + m ^ 2
rw [expand]
ringhave name : statement := proof proves an intermediate fact and adds it to the
context. It is the Lean spelling of “note that”, and it is the main tool for
making a proof readable: state the intermediate quantity, then use it.
show restates the goal in a definitionally equal form. It changes nothing
that Lean can see and everything a reader can: it says, at this point in the
argument, what you believe you are proving. Use it after a chain of unfoldings.
variance_shortcut is ring identity, and this proof takes
the long way round on purpose, to show the two structuring tactics in a proof
small enough to hold in your head.
have expand : (x - m) ^ 2 = (x - m) * (x - m) := sq (x - m) ▸ rflThe intermediate fact is “a square is a product with itself”. Its proof is a term:
sqis the Mathlib lemmaa ^ 2 = a * a, sosq (x - m)is that equation ata := x - m, ande ▸ rflrewrites the goal witheand then closes what is left by reflexivity — here the goal becomes(x - m) * (x - m) = (x - m) * (x - m), whichrflproves. Writing the proof as:= sq (x - m)on its own would have done the same job more plainly, since that term already has the stated type. Either way the goal is untouched and the context has gainedexpand.Goalx m : ℝ expand : (x - m) ^ 2 = (x - m) * (x - m) ⊢ (x - m) ^ 2 = x ^ 2 - 2 * m * x + m ^ 2show (x - m) ^ 2 = x ^ 2 - 2 * m * x + m ^ 2This restates the goal as itself. Nothing changes, and that is the point of including it here:
showis a claim about what you think the goal is, so it is free to write and it fails loudly when you are wrong. In a proof that had just unfolded three definitions it would be the most useful line in the script; in this one it is a placeholder for that habit.rw [expand]Now
expandis used: rewriting with it left to right turns the(x - m) ^ 2in the goal into(x - m) * (x - m).Goalx m : ℝ expand : (x - m) ^ 2 = (x - m) * (x - m) ⊢ (x - m) * (x - m) = x ^ 2 - 2 * m * x + m ^ 2ringMultiply out and compare normal forms.
ringwould have closed the original goal in one line — squares are part of its normal form — soexpandbought nothing mathematically. What the script demonstrates is the shape: name the intermediate step, restate the target, rewrite, close.
theorem obs_decomposition (z y1 y0 : ℝ) :
z * y1 + (1 - z) * y0 = y0 + z * (y1 - y0) := by
calc z * y1 + (1 - z) * y0
= z * y1 + y0 - z * y0 := by ring
_ = y0 + (z * y1 - z * y0) := by ring
_ = y0 + z * (y1 - y0) := by rw [mul_sub]calc writes a chain of equalities or inequalities in the usual mathematical
form, with the
justification for each step to the right of :=, and each _ standing for the
previous right-hand side. The example is the decomposition that every
discussion of potential outcomes starts from.
Claim. For any treatment indicator
Expand the second product:
Read that back and the three calc steps are the three sentences.
= z * y1 + y0 - z * y0 := by ringDistribute
. Each step of acalcis its own goal — the one shown below — andby ringcloses this one because it is a polynomial identity.Goal⊢ z * y1 + (1 - z) * y0 = z * y1 + y0 - z * y0_ = y0 + (z * y1 - z * y0) := by ringRearrange so that the control outcome is out front and the two
terms are together. The_is the previous right-hand side, so this step’s goal is the one shown, and again it is pure rearrangement. The step exists because the next one is a rewrite, and a rewrite only fires on the syntax in front of it: thez * y1 - z * y0has to be sitting in one bracket beforemul_subcan recognize it.Goal⊢ z * y1 + y0 - z * y0 = y0 + (z * y1 - z * y0)_ = y0 + z * (y1 - y0) := by rw [mul_sub]mul_sub : a * (b - c) = a * b - a * c. Rewriting left to right turns thez * (y1 - y0)on the right of this step’s equation intoz * y1 - z * y0, which makes both sides identical, andrwcloses a goal that has becomerfl.by ringwould also have worked; doing it by hand shows where the factoring happens, which in the variance calculations of chapter 9 is the step that matters.
A calc block can mix = and ≤, so a variance bound reads as a chain of
inequalities rather than as a pile of rewrites. Where you have a choice, choose
calc.
The automation
A handful of tactics do the arithmetic so you do not have to. Knowing which one to reach for is most of the practical skill of writing Lean.
theorem square_of_deviation (x m : ℝ) :
(x - m) ^ 2 = x ^ 2 - 2 * m * x + m ^ 2 := by ring
theorem indicator_algebra (z y1 y0 : ℝ) :
z * y1 + (1 - z) * y0 = y0 + z * (y1 - y0) := by ringring proves any identity that holds in every commutative ring: it normalizes
both sides and compares. Every “expand the square” step in a variance
calculation is a ring call, and ring treats division as multiplication by an
inverse, so it does not need to know that a denominator is nonzero — it also
cannot use that fact if you have it.
ring knows nothing about hypotheses and nothing about inequalities. If your
goal needs either, you want the next two.
theorem mean_between (a b : ℝ) (hab : a ≤ b) : a ≤ (a + b) / 2 := by linarith
theorem two_mul_le_sq_add_sq (a b : ℝ) : 2 * (a * b) ≤ a ^ 2 + b ^ 2 := by
nlinarith [sq_nonneg (a - b)]linarith closes any goal that follows from the hypotheses by adding
inequalities and scaling them by positive constants — linear arithmetic over an
ordered field. It looks at every hypothesis in context, so the usual way to use
it is to have the facts you need and then call it.
nlinarith is linarith plus products of hypotheses and squares. The idiom
nlinarith [sq_nonneg (a - b)] — hand it the square you know is non-negative —
is how most Cauchy–Schwarz-flavoured bounds get closed.
theorem variance_denominator_pos (n : ℕ) (hn : 0 < n) : 0 < (n : ℝ) ^ 2 + 1 := by
positivity
theorem mean_eq_iff (a b m : ℝ) (h : (a + b) / 2 = m) : a + b = 2 * m := by
field_simp at h
linarithpositivity proves 0 < e, 0 ≤ e or e ≠ 0 by walking the syntax of e:
sums of squares, sample sizes, products of positives. It is the right tactic for
the side conditions that field_simp and div_pos demand.
field_simp clears denominators, given that they do not vanish. It looks for
the nonvanishing facts in context, which is why proofs about means begin with a
line like have hn : (n : ℝ) ≠ 0 := …. Afterwards the goal is polynomial, so
field_simp is almost always followed by ring or linarith.
variance_denominator_pos is one positivity call: the expression
0 < n is not even needed.
mean_eq_iff is the two-line pattern.
field_simp at hThe hypothesis is
(a + b) / 2 = m. Multiplying through by the denominator turns it intoa + b = 2 * m; the2needs no side condition, sincefield_simpcan see for itself that it is not zero. Note that this rewrote the hypothesis, and that what came out happens to be the goal.Goala b m : ℝ h : a + b = 2 * m ⊢ a + b = 2 * mlinarithThe goal is now a linear consequence of
h— in fact it ish— solinarithcloses it, andexact hwould too. Writinglinarithis the robust choice:field_simpdecides for itself how to arrange a product, so the hypothesis it leaves may bea + b = 2 * mora + b = m * 2depending on the version, and a tactic that does linear arithmetic does not care which.
theorem concrete_mean : ((2 : ℝ) + 4 + 6) / 3 = 4 := by norm_num
theorem control_count (n n₁ : ℕ) (h : n₁ ≤ n) : n₁ + (n - n₁) = n := by omega
theorem all_units_small : ∀ i : Fin 3, i.val < 3 := by decidenorm_num evaluates concrete numerals in ℕ, ℤ, ℚ or ℝ, and extends
simp with numeric lemmas.
omega is a decision procedure for linear arithmetic over ℕ and ℤ,
including the truncated subtraction that makes ℕ treacherous. Sample sizes and
group counts live in ℕ, so omega comes up constantly:
n₁ + (n - n₁) = n given n₁ ≤ n is an omega goal and nothing else will do
it cleanly.
decide evaluates a decidable proposition. It works only when everything is
concrete and small, and — the part that matters for reading other people’s
work — it proves the claim for that instance only.
theorem cast_add (n₁ n₀ : ℕ) : ((n₁ + n₀ : ℕ) : ℝ) = (n₁ : ℝ) + (n₀ : ℝ) := by
push_cast
ring
theorem cast_sub (n k : ℕ) (h : k ≤ n) : ((n - k : ℕ) : ℝ) = (n : ℝ) - (k : ℝ) := by
rw [Nat.cast_sub h]push_cast pushes coercions towards the leaves of an expression; norm_cast
tries to eliminate them. Chapter 3 does casts properly. The one thing to carry
away now is the second example: no cast tactic will move a cast through a
ℕ-subtraction for you, because ((n - k : ℕ) : ℝ) = (n : ℝ) - k is false
without k ≤ n; the proof has to hand Nat.cast_sub the hypothesis h
explicitly. When a cast tactic makes no progress, suspect the statement before
the tactic.
push_castIn
cast_addthe cast sits outside a sum of naturals.push_castmoves it inward, so↑(n₁ + n₀)becomes↑n₁ + ↑n₀and both sides of the goal are the same sum of two casts.Goaln₁ n₀ : ℕ ⊢ ↑n₁ + ↑n₀ = ↑n₁ + ↑n₀ringCloses what is left.
rflwould also do it — after the cast has been pushed the two sides are the same term — andringis the habit worth having, because in a real proofpush_castleaves an expression that is equal to the target rather than identical to it.rw [Nat.cast_sub h]cast_subis the theorem the tactics cannot do for you.Nat.cast_sub : k ≤ n → ↑(n - k) = ↑n - ↑ktakes the inequality as an argument, soNat.cast_sub his the equation specialized to thish, and one rewrite turns the left side of the goal into the right and closes it by reflexivity.
Case splits and contradictions
theorem indicator_sq (i : Fin 3) (z : Finset (Fin 3)) :
(if i ∈ z then (1 : ℝ) else 0) * (if i ∈ z then (1 : ℝ) else 0)
= if i ∈ z then (1 : ℝ) else 0 := by
by_cases h : i ∈ z
· rw [ite_eq_left h, mul_one]
· rw [ite_eq_right h, mul_zero]by_cases h : p splits into the case where p holds and the case where it does
not. It works for any proposition: if p has a Decidable instance Lean uses
it, and otherwise it falls back on Classical.em. For an indicator this is the
entire story — Z i is 1 on the treated and 0 on the controls — so every
fact about an indicator is two computations:
case pos
i : Fin 3
z : Finset (Fin 3)
h : i ∈ z
⊢ ((if i ∈ z then 1 else 0) * if i ∈ z then 1 else 0) = if i ∈ z then 1 else 0
case neg
i : Fin 3
z : Finset (Fin 3)
h : i ∉ z
⊢ ((if i ∈ z then 1 else 0) * if i ∈ z then 1 else 0) = if i ∈ z then 1 else 0
Note that the goal is unchanged in both branches: by_cases adds a
hypothesis and nothing else. The work is done afterwards.
by_cases h : i ∈ zTwo goals, differing only in the hypothesis
h : i ∈ zorh : i ∉ z. Both targets are still the full statement about theif, as the two states above show.· rw [ite_eq_left h, mul_one]ite_eq_left h : (if c then t else e) = tgiven a proof ofc. Rewriting with it replaces all threeifs in the goal by their left branch1, leaving1 * 1 = 1; thenmul_oneturns the left side into1and the goal closes by reflexivity.Goali : Fin 3 z : Finset (Fin 3) h : i ∈ z ⊢ 1 * 1 = 1· rw [ite_eq_right h, mul_zero]The mirror:
ite_eq_right htakes a proof of¬ cand collapses theifto its right branch0, leaving0 * 0 = 0, andmul_zerofinishes. Two branches, four rewrites, and no arithmetic — which is what “an indicator is algebra with two cases” looks like when written out.
theorem from_absurd_hypotheses (n : ℕ) (h1 : 0 < n) (h2 : n = 0) : (1 : ℝ) = 2 := by
exfalso
rw [h2] at h1
exact absurd h1 (lt_irrefl 0)
theorem from_absurd_hypotheses' (n : ℕ) (h1 : 0 < n) (h2 : n = 0) : (1 : ℝ) = 2 := by
subst h2
contradiction
theorem nonneg_of_not_neg (x : ℝ) (h : ¬ (x < 0)) : 0 ≤ x := by
by_contra hc
exact h (lt_of_not_ge hc)exfalso replaces any goal by False; contradiction finds an outright
inconsistency in the context and closes the goal; by_contra h assumes the
negation of the goal and asks you for False.
The first two theorems are the same worthless theorem proved two ways, and the third is the honest use of the same machinery.
exfalsoAnything follows from
False, so a proof ofFalseis a proof of1 = 2. This throws the conclusion away — and notice that the conclusion was never looked at, which is the whole complaint about theorems like this one.Goaln : ℕ h1 : 0 < n h2 : n = 0 ⊢ Falserw [h2] at h1h2 : n = 0rewritten inh1 : 0 < ngivesh1 : 0 < 0. The two hypotheses have been brought into the same expression, which is where a contradiction becomes visible.Goaln : ℕ h1 : 0 < 0 h2 : n = 0 ⊢ Falseexact absurd h1 (lt_irrefl 0)absurd : p → ¬ p → qwithp := (0 < 0):h1is the proof,lt_irrefl 0is the refutation, andqis theFalsewe owe.
from_absurd_hypotheses' does it in two lines instead of three.
subst h2substeliminates a variable that a hypothesis equates to something:h2saysnis0, sonis replaced by0everywhere and bothnandh2disappear from the context. What is left ish1 : 0 < 0and the untouched goal.Goalh1 : 0 < 0 ⊢ 1 = 2contradictioncontradictionscans the context for a hypothesis that cannot hold — here0 < 0, which is impossible on the naturals by the definition of<— and closes any goal when it finds one. It needs noexfalso, because it is not proving the goal, it is discharging the context.
nonneg_of_not_neg is the one to imitate. It proves something true, and it
uses the negation because ¬ (x < 0) and 0 ≤ x are the same fact stated on
opposite sides of a ¬.
by_contra hcThe goal
0 ≤ xis replaced byFalse, with its negationhc : ¬ (0 ≤ x)added to the context. On a linear order this is the classical step, and it is where the argument gets its leverage: the hypothesis and the assumed negation are now both statements about the sign ofx.Goalx : ℝ h : ¬x < 0 hc : ¬0 ≤ x ⊢ Falseexact h (lt_of_not_ge hc)lt_of_not_ge : ¬ (b ≤ a) → a < bturnshcinto a proof ofx < 0. Andhis¬ (x < 0), which is(x < 0) → False, so applyinghto that proof produces theFalsewe owe. Nothing here is specific toℝ; it is the¬ ¬ p → pstep of chapter 1’s stretch exercise, done byby_contra.
Then read the first two theorems again. Both prove (1 : ℝ) = 2 from
0 < n and n = 0, and in neither is the “conclusion” ever touched. A theorem whose
hypotheses cannot all hold is provable and worthless, and no compiler will warn
you, because there is nothing wrong. This is the failure mode chapter 5 gives
you a checklist for, and it is the reason the last section of this chapter
exists.
Extensionality
theorem outcomes_eq (y1 y0 : Fin 3 → ℝ) (h : ∀ i, y1 i = y0 i) : y1 = y0 := by
funext i
exact h i
theorem assignments_eq (s t : Finset (Fin 3)) (h : ∀ i, i ∈ s ↔ i ∈ t) : s = t := by
ext i
exact h iTwo outcome vectors are equal exactly when they agree unit by unit; funext i
turns a goal about functions into a goal about their values at an arbitrary
i:
y1 y0 : Fin 3 → ℝ
h : ∀ (i : Fin 3), y1 i = y0 i
i : Fin 3
⊢ y1 i = y0 i
and then exact h i closes it, because h applied at that same i is
literally the goal. Two lines: reduce the statement about functions to a
statement about one value, then quote the hypothesis at that value.
ext is the general version, applying whatever extensionality lemma fits. For
Finset — assignments, blocks, clusters — that is membership, so ext i
reduces set equality to an equivalence of memberships.
ext is = tbetween finsets becomesi ∈ s ↔ i ∈ tfor a freshi. TheFinsetextensionality lemma is whatextlooked up; had the goal been an equality of functions it would have foundfunextinstead, which is whyextis the one to reach for by default.Goals t : Finset (Fin 3) h : ∀ (i : Fin 3), i ∈ s ↔ i ∈ t i : Fin 3 ⊢ i ∈ s ↔ i ∈ texact h ihis exactly a family of such equivalences, one per unit, soh iis the goal. Two assignments are the same assignment when the same units are treated under both — and that sentence is the proof.
Chapter 12 needs this constantly, because a randomization test is a statement about sets of assignments.
Reading an error message
theorem mul_add_the_right_way (a b : ℝ) : a * (b + 1) = a * b + a := by
rw [mul_add, mul_one]The theorem itself is two rewrites, and both are needed.
rw [mul_add]mul_add : a * (b + c) = a * b + a * cdistributes the product over the sum, so the goal’s left side becomesa * b + a * 1.Goala b : ℝ ⊢ a * b + a * 1 = a * b + arw [mul_one]mul_one : a * 1 = aclears the unit factor, both sides becomea * b + a, andrwcloses the goal by reflexivity. Arw [x, y]list is exactly this: one rewrite after another, in the order written, each firing on what the previous one left.
Lean’s errors are long and formulaic, which means they are learnable. Two patterns cover most of the first week.
rewrite failed tells you the pattern it looked for and the expression it
looked in:
error: Tactic `rewrite` failed: Did not find an occurrence of the pattern
(?a + ?b) * ?c
in the target expression
a * (b + 1) = a * b + a
The ?a, ?b are the lemma’s own variables waiting to be matched. Compare the
pattern with your goal and the mismatch is usually visible in a second: here the
lemma is add_mul, about (a + b) * c, and the goal has the factors the other
way round, so mul_add is what was wanted. When the pattern does look like
your goal but the rewrite still fails, the difference is typically implicit: a
different type, a coercion, or an instance.
Open that block in the playground and change mul_add to add_mul if you
want to watch it happen.
unsolved goals prints the state you were left in. That is a report, not a
complaint: it is telling you exactly what remains.
error: unsolved goals
n k : ℕ
⊢ ↑(n - k) = ↑n - ↑k
Here push_cast did nothing, and the reason is mathematical — it refuses to
push a cast through ℕ-subtraction, because the goal is false without k ≤ n.
An error that says a tactic made no progress is frequently an error about your
statement.
Finding the lemma
Mathlib has well over a hundred thousand theorems. You are not going to remember their names, and you do not have to: four tactics search for you.
theorem sum_of_squares_nonneg (x : ℝ) : 0 ≤ x ^ 2 := by
exact sq_nonneg x
theorem sum_add (s : Finset (Fin 5)) (f g : Fin 5 → ℝ) :
∑ i ∈ s, (f i + g i) = ∑ i ∈ s, f i + ∑ i ∈ s, g i := by
exact Finset.sum_add_distrib
theorem sum_div_two (a b c : ℝ) : (a + b) / c = a / c + b / c := by
rw [add_div]
theorem sub_nonpos_of_le (x y : ℝ) (h : x ≤ y) : x - y ≤ 0 := by
exact sub_nonpos.mpr hexact?looks for a single lemma that closes the goal outright.apply?looks for lemmas whose conclusion matches, leaving subgoals.simp?runssimpand prints thesimp only [...]it actually used.rw?lists the rewrites that apply to the current goal.
The four theorems in that block are the four answers such a search returns, and each is one line.
exact sq_nonneg xsq_nonneg x : 0 ≤ x ^ 2is the goal outright. This is the shapeexact?reports most often: the fact was already in the library under a guessable name.exact Finset.sum_add_distribChapter 3’s splitting lemma, applied with every argument implicit — the index set and the two functions are all determined by the goal, so the name alone is the proof.
rw [add_div]add_div : (a + b) / c = a / c + b / c. Here the answer is a rewrite rather than a term: after it fires, both sides of the goal are identical andrwcloses it.rw?is the tactic that would have suggested it.exact sub_nonpos.mpr hsub_nonpos : a - b ≤ 0 ↔ a ≤ b, so the goal is its left side andhis its right, which means.mpr. Note the caveat below:simp?on this goal offers a different route, and picking the↔lemma and the projection you want is the part a search tactic cannot do for you.
The workflow is: get the goal into the shape you believe is a known fact, run
exact?, read what comes back, and paste the answer in. Leaving a search
tactic in finished code is slow and fragile; they are development tools, like a
debugger, not part of the program.
Two caveats, both visible in the block above. The search tactics find a proof, not
the proof: simp? on x - y ≤ 0 reports a rewrite that leaves you with
x ≤ y, when the lemma you actually wanted was sub_nonpos. And they only
search for what you asked: if the goal is subtly the wrong statement, exact?
failing tells you nothing about whether the right statement is provable.
Which tactic when
| The goal is | Reach for |
|---|---|
p → q, or ∀ i, … | intro |
| exactly a hypothesis, or a known lemma | exact; exact? to find it |
| the conclusion of a lemma | apply |
| a structure whose parts you can supply | refine ⟨_, ?_⟩, constructor |
∃ i, … | use |
| rewritable by an equation you have | rw, rw ← , simp only |
| a polynomial identity | ring |
| a linear consequence of the hypotheses | linarith |
| nonlinear, given some squares | nlinarith [sq_nonneg …] |
0 < e, 0 ≤ e, e ≠ 0 | positivity |
| an equation with denominators | field_simp, then ring |
| concrete numerals | norm_num |
arithmetic in ℕ or ℤ, including - | omega |
| small, concrete, decidable | decide |
cluttered with ↑ | push_cast, norm_cast |
| an equality of functions | funext |
an equality of Finsets | ext |
about if … then … else | by_cases |
| impossible, and so are the hypotheses | exfalso, contradiction |
| a chain of steps | calc |
| in need of an intermediate fact | have |
Exercises
Exerciseintro then exact (warm-up)
If every unit’s control outcome is at most its treated outcome, then in
particular unit 2’s is. Two tactics.
theorem ex_intro_exact (y1 y0 : Fin 3 → ℝ) : (∀ i, y0 i ≤ y1 i) → y0 2 ≤ y1 2 := by
sorryHint
Move the hypothesis into the context with intro h, then exact h 2 applies
it at the unit the goal is about.
Show solution
theorem ex_intro_exact (y1 y0 : Fin 3 → ℝ) : (∀ i, y0 i ≤ y1 i) → y0 2 ≤ y1 2 := by
intro h
exact h 2ExerciseThe mean lies below the larger value (warm-up)
The mean of two numbers lies below the larger of them. One tactic.
theorem ex_mean_le (a b : ℝ) (h : a ≤ b) : (a + b) / 2 ≤ b := by
sorryHint
The goal is a linear consequence of h, and linarith is the tactic whose
whole job is linear consequences.
Show solution
theorem ex_mean_le (a b : ℝ) (h : a ≤ b) : (a + b) / 2 ≤ b := by
linarithExerciseExpanding a squared deviation (core)
Expand
theorem ex_deviation_sq (x m : ℝ) :
(x - m) ^ 2 = x ^ 2 - 2 * m * x + m ^ 2 := by
sorryHint
It is an identity between polynomials in a commutative ring, which is exactly
what ring decides.
Show solution
theorem ex_deviation_sq (x m : ℝ) :
(x - m) ^ 2 = x ^ 2 - 2 * m * x + m ^ 2 := by
ringExerciseThe sharp null (core)
Under the sharp null of no effect, every unit’s treated outcome equals its
control outcome. Show that unit 0’s effect is then zero.
theorem ex_sharp_null (y1 y0 : Fin 3 → ℝ) (h : ∀ i, y1 i = y0 i) :
y1 0 - y0 0 = 0 := by
sorryHint
h is a ∀, so rw [h 0] rewrites with its instance at unit 0 — rw [h]
also works, since rw will instantiate. After the rewrite the goal is
y0 0 - y0 0 = 0, which is sub_self; both steps fit in one rw [...].
Show solution
theorem ex_sharp_null (y1 y0 : Fin 3 → ℝ) (h : ∀ i, y1 i = y0 i) :
y1 0 - y0 0 = 0 := by
rw [h 0, sub_self]ExerciseFrom an exact effect to a positive one (core)
From “some unit has effect exactly 2” conclude “some unit has a positive
effect”.
theorem ex_exists_pos (y1 y0 : Fin 3 → ℝ) (h : ∃ i, y1 i - y0 i = 2) :
∃ i, 0 < y1 i - y0 i := by
sorryHint
Destruct the hypothesis with obtain ⟨i, hi⟩ := h, offer the same witness back
with use i, then rewrite with hi and let norm_num finish. Four lines, one
per idea.
Show solution
theorem ex_exists_pos (y1 y0 : Fin 3 → ℝ) (h : ∃ i, y1 i - y0 i = 2) :
∃ i, 0 < y1 i - y0 i := by
obtain ⟨i, hi⟩ := h
use i
rw [hi]
norm_numExerciseClearing a denominator (core)
From s / n = m and n ≠ 0, conclude s = n * m: clear the denominator.
theorem ex_mean_clear (s n m : ℝ) (hn : n ≠ 0) (h : s / n = m) : s = n * m := by
sorryHint
field_simp at h will find hn : n ≠ 0 in the context by itself. Look at the
hypothesis it produces before you write the exact that finishes: field_simp
normalizes the order of a product, and it does not consult the goal about which
order you would have preferred.
Show solution
theorem ex_mean_clear (s n m : ℝ) (hn : n ≠ 0) (h : s / n = m) : s = n * m := by
field_simp at h
exact hExerciseThe indicator is idempotent (stretch)
theorem ex_indicator_idempotent (i : Fin 4) (z : Finset (Fin 4)) :
(if i ∈ z then (1 : ℝ) else 0) ^ 2 = if i ∈ z then (1 : ℝ) else 0 := by
sorryHint
Split with by_cases h : i ∈ z, then collapse the if in each branch —
ite_eq_left h and ite_eq_right h, or simp [h] if you would rather not
remember the names. What is left is 1 ^ 2 = 1 (one_pow) and 0 ^ 2 = 0
(norm_num).
Show solution
theorem ex_indicator_idempotent (i : Fin 4) (z : Finset (Fin 4)) :
(if i ∈ z then (1 : ℝ) else 0) ^ 2 = if i ∈ z then (1 : ℝ) else 0 := by
by_cases h : i ∈ z
· rw [ite_eq_left h, one_pow]
· rw [ite_eq_right h]; norm_numReading autoformalized Lean
Tactics make it easy to close a goal without noticing what the goal said. Both of the following compile, and both are worthless in a way the compiler cannot see.
The habit both pitfalls point to is the same one: when you are handed a formalization, read the statement, decide what it should require, and only then look at the proof. A tactic script is a plausible-looking artefact even when it is proving nothing at all.
Next: ℝ, Finset, and the sums that every result in Part II is made of.