It would be convenient to declare the variables below so that inline prose
throughout this chapter can use a, b, c, n, m, α, e1, e2, x,
and y without repeating their type annotations, but the same problem
described elsewhere in this chapter applies: an unused variable is silently
added to the local context in basically every proof from here on, even when the
theorem never mentions it. Until we have a
way to declare variables visible only for inline prose (rather than for every
lean block), we leave this commented out:
-- variable (a b c : Prop) (n m : Nat) (α : Type) (e1 e2 x y : α)
Yipeng Liu (berberman) said: Maybe we should implement a separate scope for
declaring variables only visible to lean role instead of lean block.
The familiar equality operator = is a (binary) function that returns
a Prop. The expression n = m is notation for Eq n m.
Because Eq can be used with elements of any type, it is also
polymorphic:
Eq.{u_1}{α:Sort u_1}:α→α→Prop#checkEq
Eq.{u_1}{α:Sort u_1}:α→α→Prop
The injectivity/disjointness principles from the Tactics chapter
apply to equality hypotheses too, and cases can exploit them
directly:
There are more examples of this kind of reasoning yet to come.
As a convenience, Lean will cast booleans to propositions by equating them to true,
which is why checking them against Prop succeeds.
For clarity, we will generally avoid relying on these implicit casts.
(n:Nat)→n.pred+1 : Sort u_1#check_failure∀n:Nat,failed to synthesize instance of type classHAddNatNat(Sort ?u.2)Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.n.pred+1
The infix notation ∧ is actually just syntactic sugar for
And a b. That is, And is a Lean operator that takes two
propositions as arguments and yields a proposition.
And(ab:Prop):Prop#checkAnd
And(ab:Prop):Prop
The sole constructor for conjunction is And.intro,
which concludes a ∧ b given that a and b hold individually.
Lean can figure out which constructor to use just from the goal's type, so we
don't have to name it ourselves. This is what the tactic constructor
does automatically: it applies whatever constructor builds a value of the
goal's type, leaving one subgoal per argument of that constructor. Since
And has just one constructor, constructor always picks it
here.
The tactics we've just used — constructor, applying
And.intro, and the anonymous constructor ⟨_, _⟩ — all conclude
a ∧ b from proofs of a and b. We say that these
tactics introduce a conjunction: they derive it as a logical
consequence of hypotheses we already have.
We also sometimes want to go the other way: given a conjunctive
hypothesis, use it to help prove something else, by extracting the two
proofs it packages together. In Lean, this is done with
obtain. We say that obtaineliminates a
conjunction: it takes the conjunction apart to expose the proofs
inside.
You've already seen the related terms construct and destruct (or
destructure), used for building or taking apart a value via its
constructors — e.g., destructuring a pair in Lists.
Building a proof with a constructor like And.intro is one way
to introduce a proposition; taking a proof apart via its constructors,
as obtain does, is one way to eliminate a hypothesis. We'll
use whichever pair of terms fits the context — "introduce"/"eliminate"
when talking about a connective's proof rules, "construct"/"destruct"
when talking about the underlying constructors.
Another important connective is the disjunction, or logical or,
of two propositions: a ∨ b is true when either a or b is.
This infix notation stands for Or a b, where
Or : Prop → Prop → Prop.
To eliminate a disjunctive hypothesis — i.e., to use it in a proof —
we proceed by case analysis, which, as with other data types like
Nat, is done using cases. The two cases are inl
(for "left injection", or "in the left case") and inr (for "right
injection", or "in the right case").
Conversely, to introduce a disjunction — i.e., to show that it holds —
it suffices to show that one of its sides holds. This can be done via
the tactics left and right. As their names imply,
the first one requires proving the left side of the disjunction, while
the second requires proving the right side. Here is a trivial use...
Up to this point, we have mostly been concerned with proving
"positive" statements — addition is commutative, appending lists
is associative, etc. We are sometimes also interested in negative
results, demonstrating that some proposition is not true. Such
statements are expressed with the logical negation operator ¬,
which is prefix notation for Not.
To see how negation works, recall the principle of explosion
from the Tactics chapter, which asserts that, if we assume a
contradiction, then any other proposition can be derived.
Following this intuition, we could define ¬ a ("not a") as
∀ c, a → c.
Lean makes an equivalent but slightly different choice,
defining ¬ a as a → False, where False is a specific
unprovable proposition defined in the standard library.
Eliminating a False hypothesis
works differently from eliminating the connectives above.
Since False carries no information, there's nothing to
extract. Rather, since False is a contradictory proposition,
the principle of explosion applies to it:
using cases on a False in the context completes
any goal:
theoremzero_not_one:0≠1:=by⊢ 0≠1/- The proposition `0 ≠ 1` is exactly the same as `¬ (0 = 1)`
— that is, `Not (0 = 1)` — which unfolds to `(0 = 1) → False`. -//- To prove an inequality, we may assume the opposite equality... -/introcontracontra:0=1⊢ False/- ...and deduce a contradiction from it. Here, the equality
`0 = 1` corresponds to `zero = succ zero`, which contradicts
disjointness of constructors `zero` and `succ`, so `contradiction`
takes care of it. -/contradictionAll goals completed! 🐙
It takes a little practice to get used to working with negation in Lean.
Even though you may see perfectly well why a claim involving
negation holds, it can be a little tricky at first to see how to make
Lean understand it!
Here are proofs of a few familiar facts to help get you warmed up.
Since inequality involves a negation, getting comfortable
with it also often requires a little practice.
A useful trick: if you are trying to prove a nonsensical goal,
apply ex_falso_quodlibet to change the goal to False. This
makes it easier to use assumptions of the form ¬ a, and in
particular of the form x ≠ y.
Besides False, Lean's standard library also defines True,
a proposition that is trivially true. To prove it, we use
the constructor True.intro explicitly, or the anonymous
constructor ⟨⟩, or the constructor tactic.
Unlike False, which is used extensively, True is used
relatively rarely: it is trivial (and therefore uninteresting)
to prove as a goal, and it provides no useful information
when it appears as a hypothesis.
The handy "if and only if" connective, which asserts that two
propositions have the same truth value, is a structure containing
the two implication directions. a ↔ b is notation for Iff a b.
You can use Iff.mp to access the forward direction of the iff and
Iff.mpr to access the backwards direction — these eliminate an
iff — and Iff.intro to convert a goal of the form a ↔ b
to two goals of the form a → b and b → a, which
introduces an iff.
structureIff(ab:Prop):Propnumber of parameters: 2fields:Iff.mp : a→bIff.mpr : b→aconstructor:Iff.intro{ab:Prop}(mp:a→b)(mpr:b→a):a↔b#printIff
structureIff(ab:Prop):Propnumber of parameters: 2fields:Iff.mp : a→bIff.mpr : b→aconstructor:Iff.intro{ab:Prop}(mp:a→b)(mpr:b→a):a↔b
It would be convenient to declare the variables below so that later code
blocks can use α, β, x, y, l, f, g, and p without repeating
their type annotations, but doing so adds all of them to every proof
context and leanOutput.
-- variable (α β : Type) (x x' y : α) (l l' : List α) (f g : α → β) (p : α → Prop)
openNatinexample:Even4:=by⊢ Even4exists2All goals completed! 🐙-- `4 = double 2` holds by `rfl`,-- but is proven automatically by `exists`
Conversely, to eliminate an existential hypothesis ∃ x, a in
the context, we destructure it to obtain a witness x and a
hypothesis stating that a holds of x.
eliminated with intro ⟨ha, hb⟩ or obtain ⟨ha, hb⟩ := h
a ∨ b (disjunction):
introduced with left and right
eliminated with cases or obtain h | h := h
False (falsehood):
eliminated with cases or contradiction
¬ a (negation):
defined as a → False
True (truth):
introduced as True.intro or with constructor
a ↔ b (iff):
introduced with constructor
eliminated with intro ⟨hab, hba⟩, obtain ⟨hab, hba⟩ := h, or Iff.mp and Iff.mpr
∃ x : α, a (existential):
introduced with exists y
eliminated with intro ⟨x, hx⟩ or obtain ⟨x, hx⟩ := h
Fundamental connectives we've been using since the beginning:
equality (x = y)
implication (a → b)
universal quantification (∀ x, a)
Together, these connectives and quantifiers are exactly the vocabulary of
what's usually called first-order logic. Later in this chapter, we'll say
more about what that means, and about how Lean's own logic goes beyond it.
Lean checks the statements of the Nat.add_comm and Nat.add_assoc theorems
in the same way that it checks the type of any term (e.g., Nat.add).
If we leave off the colon and the type, Lean prints these types
in the infoview for us.
Why?
The reason is that the identifier Nat.add_comm actually refers to a
proof object — a logical derivation establishing the truth of the
statement ∀nm:Nat,n+m=m+n. The type of this object
is the proposition that it is a proof of.
The type of an ordinary function tells us what we can do with it.
If we have a term of type Nat→Nat→Nat, we can give it
two Nats as arguments and get a Nat back.
Similarly, the statement of a theorem tells us what we can use
that theorem for.
If we have a term of type ∀nm:Nat,n=m→n+n=m+m,
and we provide it two numbers n and m and a third "argument"
of type n = m, we get back a proof object of type n + n = m + m.
Lean actually allows us to apply a theorem as if it were
a function. This is often handy in proof scripts — e.g., suppose
we want to prove the following:
It appears at first sight that we ought to be able to prove this
by rewriting with Nat.add_comm twice to make the two sides match.
The problem is that the second rewrite undoes the effect
of the first, leaving us back where we started...
We encountered similar issues back in the Induction chapter, and we
saw that we can fix them by applying Nat.add_comm to the arguments we want it
to be instantiated with, in much the same way as we apply
a polymorphic function to a type argument. Then the rewrite is forced
to happen exactly where we want it.
n m : Nat
h₁ : n = m
h₂ : m = 42
trans_eq : ∀ {α : Type} {x y z : α}, x = y → y = z → x = z
What is the type of this proof object?
@trans_eq Nat m 42 n h₂
m = n
m = n → 42 = n
42 = n → m = n
Does not typecheck
Show solution
example(nm:Nat)(Variable name `h₁` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_h₁Note: This linter can be disabled with `set_option linter.unusedVariables false`h₁:n=m)(h₂:m=42)(trans_eq:∀{α:Type}{xyz:α},x=y→y=z→x=z):42=n→m=n:=@trans_eqNatm42nh₂
Quiz
Suppose, again, we have
n m : Nat
h₁ : n = m
h₂ : m = 42
trans_eq : ∀ {α : Type} {x y z : α}, x = y → y = z → x = z
What is the type of this proof object?
@trans_eq _ 42 n m
n = m → m = 42 → n = 42
42 = n → n = m → 42 = m
n = 42 → 42 = m → n = m
Does not typecheck
Show solution
example(nm:Nat)(Variable name `h₁` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_h₁Note: This linter can be disabled with `set_option linter.unusedVariables false`h₁:n=m)(Variable name `h₂` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_h₂Note: This linter can be disabled with `set_option linter.unusedVariables false`h₂:m=42)(trans_eq:∀{α:Type}{xyz:α},x=y→y=z→x=z):42=n→n=m→42=m:=@trans_eq_42nm
Quiz
Suppose, again, we have
n m : Nat
h₁ : n = m
h₂ : m = 42
trans_eq : ∀ {α : Type} {x y z : α}, x = y → y = z → x = z
What is the type of this proof object?
trans_eq h₂ h₁
m = n
42 = n
n = 42
Does not typecheck
Show solution
example(nm:Nat)(h₁:n=m)(h₂:m=42)(trans_eq:∀{α:Type}{xyz:α},x=y→y=z→x=z):True:=unsolved goalsnm:Nath₁:n=mh₂:m=42trans_eq:∀{α:Type}{xyz:α},x=y→y=z→x=z⊢ Truebyn:Natm:Nath₁:n=mh₂:m=42trans_eq:∀{α:Type}{xyz:α},x=y→y=z→x=z⊢ Truehave:=trans_eqh₂Application type mismatch: The argumenth₁has typen=mbut is expected to have type42=?m.13in the applicationtrans_eqh₂h₁h₁n:Natm:Nath₁:n=mh₂:m=42trans_eq:∀{α:Type}{xyz:α},x=y→y=z→x=z⊢ True
Application type mismatch: The argumenth₁has typen=mbut is expected to have type42=?m.13in the applicationtrans_eqh₂h₁
We've seen two different ways of expressing logical claims in Lean:
with booleans (of type Bool), and with propositions (of type Prop).
Here are the key differences between Bool and Prop:
| | `Bool` | `Prop` |
| ------------------- | ------ | ------ |
| decidable? | yes | no |
| usable with match? | yes | no |
Since functions in Lean by default must terminate on all inputs,
a terminating function of type Nat→Bool is a decision procedure —
i.e., it yields true or false on all inputs.
For example, Nat.even is a decision procedure for the property
"is even".
Since Prop includes both decidable and undecidable properties,
we have two options when we want to formalize a property that happens
to be decidable: we can express it either as a boolean computation,
or as a function into Prop.
For instance, to claim that a number n is even,
we can say either that Nat.even n evaluates to true...
Of course, it would be deeply strange if these two characterizations
of evenness did not describe the same set of natural numbers!
Fortunately, they do!
(We use Nat.beq_eq_true_eq because n == m is a wrapper of DecidableEqNat.
We will go over this in the Typeclasses chapter.)
So what should we do in situations where some claim could be formalized
as either a proposition or a boolean computation?
Which should we choose?
In general, both can be useful. For example, booleans are more useful
for defining functions, since we can test whether they are true using
conditional expressions.
An important benefit of stating facts using booleans
is enabling some proof automation through computation with terms, a
technique known as proof by reflection.
Consider the following statement:
Nat.Even 100
The most direct way to prove this is to give the value of k explicitly.
Basically this is saying that computation is a good proof tactic. But this is a little confusing to me because we seem to want to eschew computation in favor of "simplification rules", which imply a preference for the Prop version, despite the downside shown here.
Now, the useful observation is that, since the two notions are equivalent,
we can use the boolean formulation to prove the other one
without mentioning the value 50 explicitly:
Although we haven't gained much in terms of proof-script simplicity
in this case, larger proofs can often be made considerably simpler
by the use of reflection.
Another advantage of booleans is that the negation of a claim about
booleans is straightforward to state and (when true) to prove:
simply flip the expected boolean result.
In contrast, propositional negation can be difficult to work with directly.
For example, suppose we state the nonevenness of 101 propositionally:
¬ Nat.Even 101
Proving this directly — by assuming that there is some n such that
101 = Nat.double n and then somehow reasoning to a contradiction —
would be rather complicated.
But if we convert it to a claim about the boolean Nat.even function,
we can let Lean do the work for us.
Conversely, there are situations where it can be easier to work with
propositions rather than booleans. In particular, knowing that
(n == m) = true is generally of little direct help in the middle of
a proof involving n and m. But if we convert the statement to
the equivalent form n = m, then we can easily rewrite with it.
Lean's logical core is a "metalanguage for mathematics" in
the same sense as familiar foundations for paper-and-pencil math, like
Zermelo–Fraenkel Set Theory (ZFC).
Mostly, the differences are not too important,
but a few points are useful to understand.
Lean's logic is quite minimalistic. This means that one occasionally
encounters cases where translating standard mathematical reasoning
into Lean is cumbersome — or even impossible — unless we enrich
its core logic with additional axioms.
A first instance has to do with equality of propositions. For example:
This is an equality between two conjunctions, which itself is also
a proposition. It states that commuted conjunctions are equal propositions,
meaning that they hold, and do not hold, in exactly the same circumstances.
Unfortunately, we cannot prove this equality directly.
However, we can prove that a ∧ b implies b ∧ a, and vice versa — this is
the commutativity of conjunction that we have seen earlier.
and_comm{ab:Prop}:a∧b↔b∧a#checkand_comm
and_comm{ab:Prop}:a∧b↔b∧a
If we think about it, this is what we mean when we say two propositions are equal —
that one holds if and only if the other holds. It would be convenient to apply this
meaning of equality to proofs so that we can rewrite propositions from
one side of ↔ to the other. To allow this, Lean provides an axiom to turn ↔ into =,
which is called propositional extensionality (propext).
axiompropext : ∀{ab:Prop},(a↔b)→a=b#printpropext
axiompropext : ∀{ab:Prop},(a↔b)→a=b
Lean provides an ext tactic that applies propext for us.
We can use it to show that commuted conjoined propositions are equal.
The pattern of deriving an equality of propositions out of ↔
then rewriting by that equality is so common that Lean will implicitly
cast ↔ to =, allowing you to rewrite on ↔ directly.
Notice that rw is also able to close goals of the form a ↔ a by reflexivity.
We can also write propositions claiming that two functions are equal
to each other. In some cases, we can also prove that two functions are
equal by reflexivity when both reduce to the same expression:
example:(funx=>x+2)=(funx=>2+x):=by⊢ (funx=>x+2)=funx=>2+xTactic `rfl` failed: The left-hand sidefunx=>x+2is not definitionally equal to the right-hand sidefunx=>2+x⊢ (funx=>x+2)=funx=>2+xrfl⊢ (funx=>x+2)=funx=>2+x
In common mathematical practice, two functions f and g are
considered equal if they produce the same output on every input, regardless
of how they happen to compute that output:
(∀ x, f x = g x) → f = g
This is known as functional extensionality, which Lean provides as funext.
The following reasoning principle is not derivable with the tools we've seen so far:
defExcludedMiddle:=∀a:Prop,a∨¬a
Logical systems in which excluded middle does not hold are referred to as
constructive logics. They are so called because to prove a proposition,
we must give a construction for it; for instance, ∃ x, p x
is proven by providing a particular value of x.
Logical systems in which excluded middle does hold,
such as ZFC set theory, are referred to as classical.
Both variants, classical and constructive, are examples of
first-order logic: propositions are built from a fixed stock of
connectives (∧, ∨, ¬, →, ↔) and quantifiers (∀, ∃) that range
over the individual elements of some domain (natural numbers, lists, and
so on), but never over propositions or predicates themselves. Classical
first-order logic — first-order logic together with excluded middle — is
the logic usually taught in an introductory logic course, and it
underlies foundations like ZFC.
Lean's own logic goes further than this, because propositions are
themselves Lean terms of type Prop, so we can quantify over them
directly. ExcludedMiddle above, ∀ a : Prop, a ∨ ¬ a, does exactly
that: it quantifies over all propositions, not over the elements of
some fixed domain. Logics that allow quantifying over propositions or
predicates, rather than only over individuals, are called
higher-order; Lean's logic is a higher-order one, of which first-order
logic is a fragment.
Lean provides classical reasoning principles in the Classical library,
including excluded middle.
Classical.em(p:Prop):p∨¬p#checkClassical.em
Classical.em(p:Prop):p∨¬p
Source revision: e85fe77, committed 2026-10-06 21:16 UTC