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.
We have now seen many examples of factual claims (i.e.,
propositions) and ways of presenting evidence of their truth
(proofs). In particular, we have worked extensively with
equality propositions (e1 = e2), implications (a → b), and
quantified propositions (∀ x, a). In this chapter, we will
see how Lean can be used to carry out other familiar forms of
logical reasoning.
Before diving into details, we should talk a bit about the status
of mathematical statements in Lean. Lean is a typed language,
which means that every sensible expression has an associated type.
Logical claims are no exception: any statement we might try to
prove in Lean has a type, namely Prop, the type of
propositions. We can see this with the #check command:
Indeed, propositions don't just have types — they are
first-class entities that can be manipulated in all the same ways as
any of the other things in Lean's world.
So far, we've seen one place where propositions can appear:
in theorem declarations.
But propositions can be used in other ways. For example, we
can give a name to a proposition using a def, just as we
give names to other kinds of expressions.
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
Equality turns out to be an inductively defined proposition, with a single constructor,
Eq.refl, standing for the proof that anything is equal to itself.
Recall from the Tactics chapter that the constructors
of an inductive type are injective and disjoint, and that
injection and contradiction let us exploit those
facts about hypotheses concerning Nat, List, and so on.
The very same injectivity and disjointness reasoning applies to a hypothesis of the form
a = b. In fact, cases can carry out this reasoning
directly on an equality hypothesis, without our having to name
injection or contradiction. Here are a few
examples.
We'll see this same disjointness principle put to use again shortly,
via contradiction, to prove 0≠1 in the Falsehood
and Negation section below.
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.
You may wonder why we bothered packing the two hypotheses n = 0 and
m = 0 into a single conjunction, since we could also have stated the
theorem with two separate premises:
For this specific theorem, both formulations are fine. But
it's important to understand how to work with conjunctive
hypotheses because conjunctions often arise from intermediate
steps in proofs, especially in larger developments. Here's a
simple example:
Another common situation is that we know a ∧ b but in some
context we need just a or just b. In such cases we can use
an underscore pattern _ to indicate that the unneeded conjunct
should just be thrown away.
Finally, we sometimes need to rearrange the order of conjunctions
and/or the grouping of multi-way conjunctions. We can see this
at work in the proofs of the following commutativity and
associativity theorems.
In the following proof of associativity, notice how projections can be
chained in sequence to obtain components of nested conjunctions.
Complete the proof.
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").
We can see in this example that, when we perform case
analysis on a disjunction a ∨ b, we must separately discharge
two proof obligations, each showing that the conclusion holds
under a different assumption — a in the first subgoal and b
in the second.
Rather than performing case analysis via cases, we can also use obtain
to match on the two possible injections, much like with obtain and ∧.
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.
Write an informal proof of double_neg:
Theorem: a implies ¬ ¬ a, for any proposition a.
Proof: Suppose some proposition a holds. We must show ¬ ¬ a —
i.e., ¬ a → False, so suppose ¬ a as well and try to derive False.
Then we have both a and ¬ a (i.e., a → False) from which
we can indeed derive False. So ¬ ¬ a holds.
Write an informal proof of the proposition
∀a:Prop,¬(a∧¬a).
Proof: Suppose, for some a, that a ∧ ¬ a holds.
Recall that ¬ a is defined as a → False.
Given a and a → False, we can prove False,
so (a ∧ ¬ a) → False, i.e. ¬ (a ∧ ¬ a).
Exercise★★(de_morgan_not_or)
De Morgan's Laws, named for Augustus De Morgan, describe how
negation interacts with conjunction and disjunction. The
following law says that "the negation of a disjunction is the
conjunction of the negations." There is a dual law
de_morgan_not_and_not to which we will return at the end of this
chapter.
Since we are working with natural numbers, we can disprove that
Nat.succ and Nat.pred are inverses of each other. This proof
will require you to come up with a specific counterexample to the
claim being disproved:
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.
However, True can be quite useful when defining complex Props using
conditionals or as a parameter to higher-order Props. We'll come back
to this later.
For now, let's take a look at how we can use True and False to
achieve an effect similar to that of the contradiction tactic, without
literally using contradiction.
Pattern-matching lets us do different things for different
constructors. If the result of applying two different
constructors were hypothetically equal, then we could use match
to convert an unprovable statement (like False) to one that is
provable (like True).
To generalize this to other constructors, we simply have to provide
an appropriate variant of DiscrFun. To generalize it to other
conclusions, we can use exfalso to replace them with False.
The contradiction tactic takes care of all of this for us.
In List.IsNil changing the _ => arm to _ :: _ => would introduce a hidden dependency to List.All (and List.In) which is not emitted to the grading variant because it's in a solution block.
This would lead to the solution of List.All_In (and List.in_mem in IndProp) to not pass comparator because the underlying terms are different.
TLDR: Don't change List.IsNil to use _ :: _ =>.
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.
In Lean, Iff is a structure packaging two fields and a
constructor. Given an Iff hypothesis, you eliminate it to
access its component implications: the "forward direction" via the
Iff.mp (short for modus ponens, the Latin name for reasoning
by implication) field, and the "reverse direction" via the
Iff.mpr (modus ponens reverse) field.
If your goal is an Iff, you introduce it by proving both
implication directions: convert the goal into two subgoals, one for
each direction, via the Iff.intro constructor, or just use the
constructor tactic.
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)
Another fundamental logical connective is existential quantification.
To say that there is some x of type α such that some property a
holds of x, we write ∃ x : α, a. This is notation for the Exists
connective, and is defined as Exists (fun (x : α) => a).
As with ∀ x : α, the type annotation : α can be omitted if Lean
is able to infer from the context what the type of x should be.
To introduce a statement of the form ∃ x, a, we must show that a
holds for some specific choice for x, known as the witness of the
existential. This is done in two steps: First, we explicitly tell Lean
which witness y we have in mind by invoking the tactic exists y.
Then we prove that a holds after all occurrences of x
are replaced by y. The exists tactic tries to close the proof
with simple tactics such as rfl or contradiction, so we may not
have to prove a explicitly.
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.
The logical connectives that we have seen provide a rich vocabulary
for defining complex propositions from simpler ones.
To illustrate, let's look at how to express the claim that an element x
occurs in a list l.
Notice that this property has a simple recursive structure:
If l is the empty list, then x cannot occur in it,
so the property "x appears in l" is simply false.
Otherwise, l has the form x' :: l'.
In this case, x occurs in l if it is equal to x'
or if it occurs in l'.
We can translate this directly into a straightforward recursive function
taking an element and a list and returning... a proposition!
This way of defining propositions recursively is very convenient in
some cases, less so in others. In particular, it is subject to the
usual restrictions regarding definitions of recursive functions,
e.g., the requirement that they be "obviously terminating."
In the next chapter, we will see how to define propositions
inductively — a different technique with its own strengths and
limitations.
We noted above that functions returning propositions can be seen as
properties of their arguments. For instance, if p has type
Nat→Prop, then p n says that property p holds of n.
Drawing inspiration from List.In, write a recursive function All
stating that some property p holds of all elements of a list
l. To make sure your definition is correct, prove the All_In
lemma below. (Of course, your definition should not just
restate the left-hand side of All_In.)
I found this exercise combining too many awkward details for too little conceptual payoff:
the construction is artificial
before simp is introduced, bif requires noisy rw and Boolean case equations
Exercise★★(CombineOddEven) (Optional)
Complete the definition of CombineOddEven below. It takes as arguments
two properties of numbers, Odd and Even, and it should return
a predicate p such that p n is equivalent to Odd n when n is odd
and equivalent to Even n otherwise.
Lean treats proofs as first-class objects.
There is a great deal to be said about this, but it is not necessary
to understand it all to use Lean. This section gives just a taste.
We have seen that we can use #check to ask Lean whether an expression
has a given type:
Nat.add : Nat→Nat→Nat#check(Nat.add:Nat→Nat→Nat)
We can also use it to check what theorem a particular identifier refers to:
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.
Operationally, this analogy goes even further: by applying a theorem
as if it were a function, i.e., applying it to values and hypotheses
with matching types, we can specialize its result without having to
resort to intermediate assertions. For example, suppose we wanted
to prove the following result:
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 |
The crucial difference between the two worlds is decidability.
Every (closed) expression of type Bool can be simplified in a finite
number of steps to either true or false — i.e., there is a terminating
mechanical procedure for deciding whether or not it is true.
This means that, for example, the type Nat→Bool is inhabited only by
functions that, given a Nat, always yield either true or false in
finite time; this, in turn, means (by a standard computability argument)
that there is no function in Nat→Bool that checks whether a given
number is the code of a terminating Turing machine.
By contrast, the type Prop includes both decidable and undecidable
mathematical propositions; in particular, the type Nat→Prop
does contain functions representing properties like
"the nth Turing machine halts."
The second table row follows directly from this essential difference.
To evaluate a pattern match (or conditional) on a boolean, we need to know
whether the scrutinee evaluates to true or false; this only works for
Bool, not Prop.
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.
Beyond the fact that non-computable properties are impossible
in general to phrase as boolean computations, even many computable
properties are easier to express using Prop than Bool, since
recursive function definitions are subject to significant restrictions.
For instance, the Automation chapter shows how to define the property that
a regular expression matches a given string using Prop.
Doing the same with Bool would amount to writing a regular expression
matching algorithm, which would be more complicated, harder to understand,
and harder to reason about than a simple (non-algorithmic) definition
of this property.
Conversely, an important side 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.
As an extreme example, a famous mechanized proof of the even more famous
four-color theorem uses reflection to reduce the analysis of hundreds
of different cases to a boolean computation.
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.
We'll come back to
reflection and decidable propositions in a later chapter,
but the examples above already illustrate the different strengths
of booleans and general propositions.
Being able to cross back and forth between the boolean and propositional
worlds will often be convenient in later chapters.
Exercise★★(logical_connectives)
The following theorems relate the propositional connectives studied
in this chapter to the corresponding boolean operations.
Given a boolean operator beq for testing equality of elements
of some type α, we can define a function beqList for testing
equality of lists with elements in α. Complete the definition
of the beqList function below. To make sure that your definition
is correct, prove the lemma beqList_true_iff.
Lean's logical core differs in some important ways from other formal
systems that are used by mathematicians to write down precise and rigorous
definitions and proofs — in particular from Zermelo–Fraenkel Set Theory
(ZFC), the most popular foundation for paper-and-pencil mathematics.
We conclude this chapter with a brief discussion of some of the
most significant differences between these two worlds.
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.
For example, the equality assertions that we have seen so far have
mostly involved inductive types (Nat, Bool, etc.).
But since the equality operator is polymorphic, we can use it at any type —
in particular, we can write propositions claiming that two propositions
are equal to each other:
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.
However, we cannot prove this equality by reflexivity, as the two sides
don't compute to the same term, and we cannot proceed by cases on
a or b, as they are not inductive.
example(ab:Prop):a∧b=b∧a:=bya:Propb:Prop⊢ a∧b=b∧aTactic `rfl` failed: The left-hand sideais not definitionally equal to the right-hand sideb=b∧aab:Prop⊢ a∧b=b∧arfla:Propb:Prop⊢ a∧b=b∧a
Tactic `rfl` failed: The left-hand sideais not definitionally equal to the right-hand sideb=b∧aab:Prop⊢ a∧b=b∧a
example(ab:Prop):a∧b=b∧a:=bya:Propb:Prop⊢ a∧b=b∧aTactic `cases` failed: major premise type is not an inductive typePropExplanation: the `cases` tactic is for constructor-based reasoning as well as for applying custom cases principles with a 'using' clause or a registered '@[cases_eliminator]' theorem. The above type neither is an inductive type nor has a registered theorem.Consider using the 'by_cases' tactic, which does true/false reasoning for propositions.ab:Prop⊢ a∧b=b∧acasesaa:Propb:Prop⊢ a∧b=b∧a
Tactic `cases` failed: major premise type is not an inductive typePropExplanation: the `cases` tactic is for constructor-based reasoning as well as for applying custom cases principles with a 'using' clause or a registered '@[cases_eliminator]' theorem. The above type neither is an inductive type nor has a registered theorem.Consider using the 'by_cases' tactic, which does true/false reasoning for propositions.ab:Prop⊢ a∧b=b∧a
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
(Informally, an extensional property is one that pertains to observable
behavior. Thus, propositional extensionality means that a proposition's
identity is completely determined by what we can observe from it — i.e.,
whether the proposition holds.) We can state this more explicitly:
Here is an example of where using = instead of ↔ is more convenient:
we show that it's possible to "flip" three conjoined propositions.
One way to prove this is to construct the ↔, destruct the ↔s provided by
and_comm and and_assoc, and apply the resulting implications a few times.
But this is a lot of hassle when the proof is conceptually simple:
we flip b and c, then we flip that conjunction with a, and we
finish by associativity. By using and_comm_eq, this is easily done
by rewriting equal propositions.
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.
The following theorem is an alternative "negative" formulation of beq_eq_true
that is more convenient in certain situations.
(We'll see examples in later chapters.) Hint: not_true_iff_false.
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.
Functional extensionality means that a function's identity is
completely determined by what we can observe from it — i.e., the results
we obtain after applying it.
(Its full type is actually slightly more general,
and is defined in terms of a more fundamental concept called quotients
rather than added directly as an axiom, but we will only discuss funext
here. This is also why, when printing axioms for theorems using funext,
it will instead display a Quot.sound axiom.)
'funext' depends on axioms: [Quot.sound]#printaxiomsfunext
'funext' depends on axioms: [Quot.sound]
Now we can prove some intuitively obvious equalities about functions
that would not be provable without funext.
One problem with the definition of the list-reversing function List.rev
is that it performs a call to ++ on each step.
Running ++ takes time asymptotically linear in the size of the list,
which means that List.rev is asymptotically quadratic.
We can improve this with the following two-argument definition:
This version of List.rev is said to be tail recursive, because the recursive
call to the function is the last operation that needs to be performed
(i.e., we don't have to execute ++ after the recursive call);
a decent compiler will generate very efficient code in this case.
Prove that the two definitions are indeed equivalent.
We have seen that it is not possible to test whether or not a
proposition a holds while defining a Lean function. You may be
surprised to learn that a similar restriction applies in proofs!
In other words, the following intuitive reasoning principle is not
derivable in Lean with the tools we've seen so far:
defExcludedMiddle:=∀a:Prop,a∨¬a
To understand operationally why this is the case, recall that,
to prove a statement of the form a ∨ b, we use the left and right
tactics, which effectively require knowing which side of the disjunction
holds. But the universally quantified a in ExcludedMiddle is an
arbitrary proposition, which we know nothing about. We don't have enough
information to choose which of left or right to apply.
However, in the special case where we happen to know that a is reflected
in some boolean term b, knowing whether it holds or not is trivial:
we just have to check the value of b.
Sadly, this trick only works for decidable propositions.
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
All classical reasoning principles in Classical are derived from
one axiom, the axiom of choice. This is the C in ZFC.
'Classical.em' depends on axioms: [propext,Classical.choice,Quot.sound]#printaxiomsClassical.em
'Classical.em' depends on axioms: [propext,Classical.choice,Quot.sound]
Lean also provides a by_cases tactic that applies Classical.em on a
given proposition. Theorems proven using this tactic implicitly use
classical axioms.
theoremem:∀a,a∨¬a:=by⊢ ∀(a:Prop),a∨¬aintroaa:Prop⊢ a∨¬aby_casesh:aposa:Proph:a⊢ a∨¬anega:Proph:¬a⊢ a∨¬a/- h : a -/·posa:Proph:a⊢ a∨¬aleftposa:Proph:a⊢ a;exacthAll goals completed! 🐙/- h : ¬ a -/·nega:Proph:¬a⊢ a∨¬arightnega:Proph:¬a⊢ ¬a;exacthAll goals completed! 🐙'em' depends on axioms: [propext,Classical.choice,Quot.sound]#printaxiomsem
'em' depends on axioms: [propext,Classical.choice,Quot.sound]
The following example illustrates why assuming the excluded middle may
lead to nonconstructive proofs:
Claim: There exist irrational numbers n and m such that n ^ m
(n to the power m) is rational.
Proof: It is not difficult to show that sqrt 2 is irrational.
So if sqrt 2 ^ sqrt 2 is rational, it suffices to take n = m = sqrt 2
and we are done. Otherwise, sqrt 2 ^ sqrt 2 is irrational.
In this case, we can take n = sqrt 2 ^ sqrt 2 and m = sqrt 2,
since n ^ m = sqrt 2 ^ (sqrt 2 * sqrt 2) = sqrt 2 ^ 2 = 2. QED.
Do you see what happened here? We used the excluded middle to
consider separately the cases where sqrt 2 ^ sqrt 2 is rational and
where it is not, without knowing which one actually holds!
Because of this, we finish the proof knowing that such n and m exist,
but not being sure of their actual values.
As useful as constructive logic is, it does have its limitations:
there are many statements that can easily be proven in classical logic
but that have only much more complicated constructive proofs,
and there are some that are known to have no constructive proof at all!
Fortunately, like functional extensionality, the excluded middle is known
to be compatible with Lean's logic, allowing it to be added safely as an axiom.
However, the results that we cover in Logical Foundations can be developed
entirely within constructive logic.
It takes some practice to understand which proof techniques must be
avoided in constructive reasoning, but arguments by contradiction,
in particular, are infamous for leading to nonconstructive proofs.
Here's a typical example: suppose that we want to show that there exists
x with some property p, i.e., such that p x. We start by assuming
that our conclusion is false; that is, ¬ ∃ x, p x. From this premise,
it is not hard to derive ∀ x, ¬ p x. If we manage to show that this
results in a contradiction, we arrive at an existence proof without ever
exhibiting a value of x for which p x holds!
The technical flaw here, from a constructive standpoint, is that we
claimed to prove ∃ x, p x using a proof of ¬ ¬ ∃ x, p x.
Allowing ourselves to remove double negations from arbitrary statements
is equivalent to assuming the excluded middle law, as shown in one of the
exercises below.
Once again, Lean's Classical library provides double negation elimination,
which relies on the Classical.choice axiom.
Classical.not_not{a:Prop}:¬¬a↔a#checkClassical.not_not'Classical.not_not' depends on axioms: [propext,Classical.choice,Quot.sound]#printaxiomsClassical.not_not
Classical.not_not{a:Prop}:¬¬a↔a
'Classical.not_not' depends on axioms: [propext,Classical.choice,Quot.sound]
Exercise★★★(excluded_middle_irrefutable)
The following theorem implies that it is always safe to assume
a decidability axiom (i.e., an instance of excluded middle) for any
particular proposition a. Why? Because the negation of such an axiom
leads to a contradiction. If ¬ (a ∨ ¬ a) were provable, then by
de_morgan_not_or as proven above, ¬ a ∧ ¬ ¬ a would be provable,
which would be a contradiction. So, it is safe to add a ∨ ¬ a as an axiom
for any particular a.
It is a theorem of classical logic that the following two assertions
are equivalent:
¬ ∃ x, ¬ p x
∀ x, p x
The dist_not_exists theorem proves one side of this equivalence.
Interestingly, the other direction cannot be proven in constructive logic,
but we can prove it here using by_cases.
For those who like a challenge, here is an exercise adapted from the Coq'Art
book by Bertot and Castéran (p. 123). Each of the following five statements,
together with ExcludedMiddle, can be considered as characterizing
classical logic. We can't prove any one of them in Lean without Classical,
but adding any one of them as an axiom allows us to work classically.
To see this, prove that all six propositions (these five plus
ExcludedMiddle) are equivalent.
Hint: Rather than considering all pairs of statements,
prove a single circular chain of implications that connects them all.
You should not use by_cases, as this implicitly introduces
a dependency on ExcludedMiddle.
Note to developers (Jonathan Chan)
If the hint suggests proving the implications in a loop,
why do the solutions not do this?