Logical Foundations

8. Logic in Lean🔗

import LF.Basics
import LF.Induction
import LF.Poly
import LF.Tactics
import LF.CustomTactics

8.1. The Prop Type🔗

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:

∀ (n m : Nat), n + m = m + n : Prop#check (∀ n m : Nat, n + m = m + n : Prop)

Note that all syntactically well-formed propositions have type Prop in Lean, regardless of whether they are true or not.

Simply being a proposition is one thing; being provable is something else!

2 = 2 : Prop#check (2 = 2 : Prop) 3 = 2 : Prop#check (3 = 2 : Prop) ∀ (n : Nat), n = 2 : Prop#check (∀ n : Nat, n = 2 : Prop)

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.

theorem plus_2_2_is_4 : 2 + 2 = 4 := ⊢ 2 + 2 = 4 All goals completed! 🐙

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.

def PlusClaim : Prop := 2 + 2 = 4 PlusClaim : Prop#check PlusClaim
PlusClaim : Prop

We can later use this name in any situation where a proposition is expected — for example, as the claim in a theorem declaration.

theorem plusClaim_is_true : PlusClaim := ⊢ PlusClaim All goals completed! 🐙

We can also write parameterized propositions — that is, functions that take arguments of some type and return a proposition.

For instance, the following function takes a number and returns a proposition asserting that this number is equal to three:

def Nat.IsThree (n : Nat) : Prop := n = 3 Nat.IsThree : Nat → Prop#check (Nat.IsThree)
Nat.IsThree : Nat → Prop

In Lean, functions that return propositions are said to define properties of their arguments.

For instance, here's a (polymorphic) property defining the familiar notion of an injective function.

def Injective {α β : Type} (f : α → β) : Prop := ∀ x y : α, f x = f y → x = y theorem succ_inj' : Injective Nat.succ := ⊢ Injective Nat.succ x:Naty:Nath:x.succ = y.succ⊢ x = y All goals completed! 🐙

8.1.1. Equality Propositions🔗

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#check Eq
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.

-- substitution example (x : Nat) (h : x = 0) : Nat.succ x = 1 := x:Nath:x = 0⊢ x.succ = 1 ⊢ Nat.succ 0 = 1 All goals completed! 🐙 -- injectivity example {m n : Nat} (h : Nat.succ m = Nat.succ n) : m = n := m:Natn:Nath:m.succ = n.succ⊢ m = n m:Nat⊢ m = m All goals completed! 🐙

(This is the same injectivity fact used above by the injection tactic in succ_inj'; here cases gets us the same conclusion in a single step.)

-- disjointness example (h : (0 : Nat) = 1) : False := h:0 = 1⊢ False All goals completed! 🐙 -- acyclicity example (n : Nat) (h : n = Nat.succ n) : False := n:Nath:n = n.succ⊢ False All goals completed! 🐙

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.

false = true : Prop#check (false : Prop)
false = true : Prop
true = true : Prop#check (true : Prop)
true = true : Prop

8.1.2. Quizzes🔗

Quiz

What is the type of the following expression?

Nat.pred 1 = 0
  1. Prop

  2. Nat → Prop

  3. ∀ n : Nat, Prop

  4. Nat → Nat

  5. Not typeable

Show solution
Nat.pred 1 = 0 : Prop#check Nat.pred 1 = 0
Nat.pred 1 = 0 : Prop
Quiz

What is the type of the following expression?

∀ n : Nat, (n + 1).pred = n
  1. Prop

  2. Nat → Prop

  3. ∀ n : Nat, Prop

  4. Nat → Nat

  5. Not typeable

Show solution
∀ (n : Nat), (n + 1).pred = n : Prop#check (∀ n : Nat, (n + 1).pred = n : Prop)
∀ (n : Nat), (n + 1).pred = n : Prop
Quiz

What is the type of the following expression?

∀ n : Nat, n.pred + 1
  1. Prop

  2. Nat → Prop

  3. ∀ n : Nat, Prop

  4. Nat → Nat

  5. Not typeable

Show solution
(n : Nat) → n.pred + 1 : Sort u_1#check_failure ∀ n : Nat, failed to synthesize instance of type class HAdd Nat Nat (Sort ?u.2) Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.n.pred + 1
Quiz

What is the type of the following expression?

fun n : Nat => n.pred + 1
  1. Prop

  2. Nat → Prop

  3. ∀ n : Nat, Prop

  4. Nat → Nat

  5. Not typeable

Show solution
fun n => n.pred + 1 : Nat → Nat#check (fun n : Nat => n.pred + 1 : Nat → Nat)
fun n => n.pred + 1 : Nat → Nat
Quiz

What is the type of the following expression?

fun n : Nat => n.pred + 1 = n
  1. Prop

  2. Nat → Prop

  3. ∀ n : Nat, Prop

  4. Nat → Nat

  5. Not typeable

Show solution
fun n => n.pred + 1 = n : Nat → Prop#check (fun n : Nat => n.pred + 1 = n : Nat → Prop)
fun n => n.pred + 1 = n : Nat → Prop
Quiz

Which of the following is not a proposition?

  1. 3 + 2 = 4

  2. 3 + 2 = 5

  3. 3 + 2 == 5

  4. (3 + 2 == 4) = false

  5. ∀ n, (3 + 2 == n) = true → n = 5

  6. All of these are propositions

Show solution
3 + 2 == 5 : Bool#check (3 + 2 == 5 : Bool)
3 + 2 == 5 : Bool

8.2. Logical Connectives🔗

8.2.1. Conjunction🔗

The conjunction, or logical and, of propositions a and b is written a ∧ b; it represents the claim that both a and b are true.

declaration uses `sorry`example : 3 + 4 = 7 ∧ 2 * 2 = 4 := ⊢ 3 + 4 = 7 ∧ 2 * 2 = 4 All goals completed! 🐙 -- proofs below

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 (a b : Prop) : Prop#check And
And (a b : Prop) : Prop

The sole constructor for conjunction is And.intro, which concludes a ∧ b given that a and b hold individually.

And.intro {a b : Prop} (left : a) (right : b) : a ∧ b#check And.intro
And.intro {a b : Prop} (left : a) (right : b) : a ∧ b

We can apply And.intro to carry out proofs.

example : 3 + 4 = 7 ∧ 2 * 2 = 4 := ⊢ 3 + 4 = 7 ∧ 2 * 2 = 4 ⊢ 3 + 4 = 7⊢ 2 * 2 = 4 ⊢ 3 + 4 = 7 All goals completed! 🐙 /- 3 + 4 = 7 -/ ⊢ 2 * 2 = 4 All goals completed! 🐙 /- 2 * 2 = 4 -/

Rather than applying the constructor, we can explicitly provide the arguments to the constructor as an exact proof.

example : 3 + 4 = 7 ∧ 2 * 2 = 4 := ⊢ 3 + 4 = 7 ∧ 2 * 2 = 4 All goals completed! 🐙

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.

example : 3 + 4 = 7 ∧ 2 * 2 = 4 := ⊢ 3 + 4 = 7 ∧ 2 * 2 = 4 ⊢ 3 + 4 = 7⊢ 2 * 2 = 4 ⊢ 3 + 4 = 7 All goals completed! 🐙 ⊢ 2 * 2 = 4 All goals completed! 🐙

We can also use Lean's anonymous constructor notation ⟨..., ...⟩, which works on constructors for proofs as well.

example : 3 + 4 = 7 ∧ 2 * 2 = 4 := ⊢ 3 + 4 = 7 ∧ 2 * 2 = 4 All goals completed! 🐙
Exercise★★(add_is_zero)
theorem declaration uses `sorry`Nat.add_is_zero (n m : Nat) : n + m = 0 → n = 0 ∧ m = 0 := n:Natm:Nat⊢ n + m = 0 → n = 0 ∧ m = 0 All goals completed! 🐙

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 obtain eliminates 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.

example (n m : Nat) : n = 0 ∧ m = 0 → n + m = 0 := n:Natm:Nat⊢ n = 0 ∧ m = 0 → n + m = 0 n:Natm:Nath:n = 0 ∧ m = 0⊢ n + m = 0 n:Natm:Nathn:n = 0hm:m = 0⊢ n + m = 0 All goals completed! 🐙

We can also match on h right at the point where we introduce it, instead of introducing and then destructing it:

example (n m : Nat) : n = 0 ∧ m = 0 → n + m = 0 := n:Natm:Nat⊢ n = 0 ∧ m = 0 → n + m = 0 n:Natm:Nathn:n = 0hm:m = 0⊢ n + m = 0 All goals completed! 🐙

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:

example (n m : Nat) : n = 0 → m = 0 → n + m = 0 := n:Natm:Nat⊢ n = 0 → m = 0 → n + m = 0 n:Natm:Nathn:n = 0hm:m = 0⊢ n + m = 0 All goals completed! 🐙

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:

example (n m : Nat) (h : n + m = 0) : n * m = 0 := n:Natm:Nath:n + m = 0⊢ n * m = 0 n:Natm:Nath:n = 0 ∧ m = 0⊢ n * m = 0 n:Natm:Nathn:n = 0hm:m = 0⊢ n * m = 0 n:Natm:Nathn:n = 0hm:m = 0⊢ n * 0 = 0 All goals completed! 🐙

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.

example (a b : Prop) (h : a ∧ b) : a := a:Propb:Proph:a ∧ b⊢ a a:Propb:ProphP:aright✝:b⊢ a All goals completed! 🐙

Conjunctions come with their own built-in projections, .left and .right, which we can use instead of pattern matching.

example (a b : Prop) (h : a ∧ b) : a := a:Propb:Proph:a ∧ b⊢ a All goals completed! 🐙
Exercise★(proj2) (Optional)
theorem declaration uses `sorry`right (a b : Prop) (h : a ∧ b) : b := a:Propb:Proph:a ∧ b⊢ b All goals completed! 🐙

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.

theorem and_commute (a b : Prop) (h : a ∧ b) : b ∧ a := a:Propb:Proph:a ∧ b⊢ b ∧ a a:Propb:Proph:a ∧ b⊢ ba:Propb:Proph:a ∧ b⊢ a a:Propb:Proph:a ∧ b⊢ b All goals completed! 🐙 a:Propb:Proph:a ∧ b⊢ a All goals completed! 🐙

The anonymous constructor allows us to write a much shorter proof.

theorem and_commute' (a b : Prop) (h : a ∧ b) : b ∧ a := a:Propb:Proph:a ∧ b⊢ b ∧ a All goals completed! 🐙

In the following proof of associativity, notice how projections can be chained in sequence to obtain components of nested conjunctions. Complete the proof.

Exercise★(and_associate)
theorem declaration uses `sorry`and_associate (a b c : Prop) (h : a ∧ (b ∧ c)) : (a ∧ b) ∧ c := a:Propb:Propc:Proph:a ∧ b ∧ c⊢ (a ∧ b) ∧ c a:Propb:Propc:Proph:a ∧ b ∧ c⊢ a ∧ ba:Propb:Propc:Proph:a ∧ b ∧ c⊢ c a:Propb:Propc:Proph:a ∧ b ∧ c⊢ a ∧ b All goals completed! 🐙 a:Propb:Propc:Proph:a ∧ b ∧ c⊢ c All goals completed! 🐙

8.2.2. Disjunction🔗

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").

theorem Nat.factor_is_zero (n m : Nat) (h : n = 0 ∨ m = 0) : n * m = 0 := n:Natm:Nath:n = 0 ∨ m = 0⊢ n * m = 0 cases h with /- `n = 0` -/ n:Natm:Nathn:n = 0⊢ n * m = 0 All goals completed! 🐙 /- `m = 0` -/ n:Natm:Nathm:m = 0⊢ n * m = 0 All goals completed! 🐙

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 ∧.

theorem and_is_false (b1 b2 : Bool) (h : (b1 = false) ∨ (b2 = false)) : (b1 && b2) = false := b1:Boolb2:Boolh:b1 = false ∨ b2 = false⊢ (b1 && b2) = false b1:Boolb2:Boolhb1:b1 = false⊢ (b1 && b2) = falseb1:Boolb2:Boolhb2:b2 = false⊢ (b1 && b2) = false b1:Boolb2:Boolhb1:b1 = false⊢ (b1 && b2) = false All goals completed! 🐙 b1:Boolb2:Boolhb2:b2 = false⊢ (b1 && b2) = false All goals completed! 🐙

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...

theorem or_intro_l (a b : Prop) (h : a) : a ∨ b := a:Propb:Proph:a⊢ a ∨ b a:Propb:Proph:a⊢ a; All goals completed! 🐙

... and here is a slightly more interesting example requiring both left and right:

theorem Nat.zero_or_succ (n : Nat) : n = 0 ∨ n = (n - 1).succ := n:Nat⊢ n = 0 ∨ n = (n - 1).succ cases n with ⊢ 0 = 0 ∨ 0 = (0 - 1).succ ⊢ 0 = 0; All goals completed! 🐙 n:Nat⊢ n + 1 = 0 ∨ n + 1 = (n + 1 - 1).succ n:Nat⊢ n + 1 = (n + 1 - 1).succ; All goals completed! 🐙
Exercise★★(mul_is_zero)
theorem declaration uses `sorry`Nat.mul_is_zero (n m : Nat) (h : n * m = 0) : n = 0 ∨ m = 0 := n:Natm:Nath:n * m = 0⊢ n = 0 ∨ m = 0 All goals completed! 🐙
Exercise★(or_commute)
theorem declaration uses `sorry`or_commute (a b : Prop) (h : a ∨ b) : b ∨ a := a:Propb:Proph:a ∨ b⊢ b ∨ a All goals completed! 🐙

8.2.3. Falsehood and Negation🔗

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.

Not (a : Prop) : Prop#check Not @[implicit_reducible] def Not : Prop → Prop := fun a => a → False#print Not example (a : Prop) : Not a = (a → False) := a:Prop⊢ (¬a) = (a → False) All goals completed! 🐙 example (a : Prop) : (¬ a) = (a → False) := a:Prop⊢ (¬a) = (a → False) All goals completed! 🐙
Not (a : Prop) : Prop
@[implicit_reducible] def Not : Prop → Prop :=
fun a => a → False

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:

theorem ex_falso_quodlibet (a : Prop) (h : False) : a := a:Proph:False⊢ a All goals completed! 🐙

The Latin ex falso quodlibet means, literally, "from falsehood follows whatever you like"; this is another common name for the principle of explosion.

Exercise★★(not_implies_other_not) (Optional)
theorem declaration uses `sorry`not_implies_other_not (a : Prop) (h : ¬ a) : (∀ c : Prop, a → c) := a:Proph:¬a⊢ ∀ (c : Prop), a → c All goals completed! 🐙

Inequality is a very common form of negated statement, so there is a special notation for it: ≠, which is infix notation for Ne.

@[reducible] def Ne.{u} : {α : Sort u} → α → α → Prop := fun {α} a b => ¬a = b#print Ne
@[reducible] def Ne.{u} : {α : Sort u} → α → α → Prop :=
fun {α} a b => ¬a = b
theorem zero_not_one : 0 ≠ 1 := ⊢ 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... -/ contra: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. -/ All 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.

theorem not_False : ¬ False := ⊢ ¬False h:False⊢ False; All goals completed! 🐙 theorem contradiction_implies_anything (a b : Prop) (h : a ∧ ¬ a) : b := a:Propb:Proph:a ∧ ¬a⊢ b a:Propb:Propha:ahna:¬a⊢ b a:Propb:Prophna:¬aha:False⊢ b All goals completed! 🐙 theorem double_neg (a : Prop) (ha : a) : ¬ ¬ a := a:Propha:a⊢ ¬¬a a:Propha:ah:¬a⊢ False; a:Propha:ah:¬a⊢ a; All goals completed! 🐙
Exercise★★(double_neg_informal) (Advanced, Optional, Manually graded)

Write an informal proof of double_neg: Theorem: a implies ¬ ¬ a, for any proposition a.

Exercise★(contrapositive)
theorem declaration uses `sorry`contrapositive (a b : Prop) (h : a → b) : (¬ b → ¬ a) := a:Propb:Proph:a → b⊢ ¬b → ¬a All goals completed! 🐙
Exercise★(not_PNP_informal) (Advanced, Manually graded)

Write an informal proof of the proposition ∀ a : Prop, ¬ (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.

theorem declaration uses `sorry`de_morgan_not_or {a b : Prop} (h : ¬ (a ∨ b)) : ¬ a ∧ ¬ b := a:Propb:Proph:¬(a ∨ b)⊢ ¬a ∧ ¬b All goals completed! 🐙
Exercise★(not_succ_inverse_pred) (Optional)

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:

theorem declaration uses `sorry`not_succ_pred_n : ¬ (∀ n : Nat, n.pred + 1 = n) := ⊢ ¬∀ (n : Nat), n.pred + 1 = n All goals completed! 🐙

Since inequality involves a negation, it also requires a little practice to be able to work with it fluently. Here is one useful trick.

If you are trying to prove a goal that is nonsensical (e.g., the goal state is false = true), apply ex_falso_quodlibet to change the goal to False.

This makes it easier to use assumptions of the form ¬ a that may be available in the context — in particular, assumptions of the form x ≠ y.

theorem not_true_is_false (b : Bool) (h : b ≠ true) : b = false := b:Boolh:b ≠ true⊢ b = false cases b with h:false ≠ true⊢ false = false All goals completed! 🐙 h:true ≠ true⊢ true = false h:true = true → False⊢ true = false h:true = true → False⊢ False h:true = true → False⊢ true = true All goals completed! 🐙

Since reasoning with ex_falso_quodlibet is quite common, Lean provides a tactic, exfalso, for applying it.

theorem not_true_is_false' (b : Bool) (h : b ≠ true) : b = false := b:Boolh:b ≠ true⊢ b = false cases b with h:false ≠ true⊢ false = false All goals completed! 🐙 h:true ≠ true⊢ true = false h:true ≠ true⊢ False h:true = true → False⊢ False h:true = true → False⊢ true = true All goals completed! 🐙
Quiz

To prove the following proposition, which tactics will we need besides intro, apply, and exact?

∀ α : Type, ∀ x y : α, x = y ∧ x ≠ y → False
  1. intro, apply, and exact suffice

  2. cases

  3. left and/or right

  4. cases and left and/or right

  5. none of the above

Show solution
example (α : Type) (x y : α) : x = y ∧ x ≠ y → False := α:Typex:αy:α⊢ x = y ∧ x ≠ y → False α:Typex:αy:αh:x = y ∧ x ≠ y⊢ False; cases h with α:Typex:αy:αh₁:x = yh₂:x ≠ y⊢ False α:Typex:αy:αh₁:x = yh₂:x ≠ y⊢ x = y; All goals completed! 🐙
Quiz

To prove the following proposition, which tactics will we need besides intro, apply, and exact?

∀ a b : Prop, a ∨ b → ¬ ¬ (a ∨ b)
  1. intro, apply, and exact suffice

  2. cases

  3. left and/or right

  4. cases and left and/or right

  5. none of the above

Show solution
example (a b : Prop) (h : a ∨ b) : ¬ ¬ (a ∨ b) := a:Propb:Proph:a ∨ b⊢ ¬¬(a ∨ b) a:Propb:Proph:a ∨ bhn:¬(a ∨ b)⊢ False; a:Propb:Proph:a ∨ bhn:¬(a ∨ b)⊢ a ∨ b; All goals completed! 🐙
Quiz

To prove the following proposition, which tactics will we need besides intro, apply, and exact?

∀ a b : Prop, a → (a ∨ ¬ ¬ b)
  1. intro, apply, and exact suffice

  2. cases

  3. left and/or right

  4. cases and left and/or right

  5. none of the above

Show solution
example (a b : Prop) : a → (a ∨ ¬ ¬ b) := a:Propb:Prop⊢ a → a ∨ ¬¬b a:Propb:Proph:a⊢ a ∨ ¬¬b; a:Propb:Proph:a⊢ a; All goals completed! 🐙
Quiz

To prove the following proposition, which tactics will we need besides intro, apply, and exact?

∀ a b : Prop, a ∨ b → (¬ ¬ a) ∨ (¬ ¬ b)
  1. intro, apply, and exact suffice

  2. cases

  3. left and/or right

  4. cases and left and/or right

  5. none of the above

Show solution
example (a b : Prop) : a ∨ b → (¬ ¬ a) ∨ (¬ ¬ b) := a:Propb:Prop⊢ a ∨ b → ¬¬a ∨ ¬¬b a:Propb:Proph:a ∨ b⊢ ¬¬a ∨ ¬¬b; cases h with a:Propb:Propha:a⊢ ¬¬a ∨ ¬¬b a:Propb:Propha:a⊢ ¬¬a; a:Propb:Propha:ahna:¬a⊢ False; a:Propb:Propha:ahna:¬a⊢ a; All goals completed! 🐙 a:Propb:Prophb:b⊢ ¬¬a ∨ ¬¬b a:Propb:Prophb:b⊢ ¬¬b; a:Propb:Prophb:bhnb:¬b⊢ False; a:Propb:Prophb:bhnb:¬b⊢ b; All goals completed! 🐙
Quiz

To prove the following proposition, which tactics will we need besides intro, apply, and exact?

∀ a : Prop, 1 = 0 → (a ∨ ¬ a)
  1. intro, apply, and exact suffice

  2. contradiction

  3. left and/or right

  4. contradiction and left and/or right

  5. none of the above

Show solution
example (a : Prop) : 1 = 0 → (a ∨ ¬ a) := a:Prop⊢ 1 = 0 → a ∨ ¬a a:Proph:1 = 0⊢ a ∨ ¬a; All goals completed! 🐙

8.2.4. Truth🔗

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.

example : True := ⊢ True All goals completed! 🐙 example : True := ⊢ True All goals completed! 🐙 example : True := ⊢ True All goals completed! 🐙

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).

def DiscrFun (n : Nat) : Prop := match n with | 0 => True | _ + 1 => False theorem discrFun_zero : DiscrFun 0 := ⊢ DiscrFun 0 All goals completed! 🐙 theorem discrFun_succ (n : Nat) : ¬ DiscrFun (n + 1) := n:Nat⊢ ¬DiscrFun (n + 1) n:Nat⊢ ¬False; n:Nath:False⊢ False; All goals completed! 🐙 theorem discr_example (n : Nat) : ¬ (0 = n + 1) := n:Nat⊢ ¬0 = n + 1 n:Nath:0 = n + 1⊢ False n:Nath:0 = n + 1hd:DiscrFun 0⊢ False n:Nath:0 = n + 1hd:DiscrFun 0⊢ DiscrFun (0 + 1) n:Nath:0 = n + 1hd:DiscrFun (n + 1)⊢ DiscrFun (0 + 1) All goals completed! 🐙

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.

Exercise★★(nil_is_not_cons) (Advanced, Optional, Manually graded)

Use the same technique as above to show that [] ≠ x :: xs. Do not use the contradiction tactic.

-- FILL IN HERE -- FILL IN HERE theorem declaration uses `sorry`nil_is_not_cons {α : Type} (x : α) (xs : List α) : ¬ ([] = x :: xs) := α:Typex:αxs:List α⊢ ¬[] = x :: xs All goals completed! 🐙

8.2.5. Logical Equivalence🔗

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.

structure Iff (a b : Prop) : Prop number of parameters: 2 fields: Iff.mp : a → b Iff.mpr : b → a constructor: Iff.intro {a b : Prop} (mp : a → b) (mpr : b → a) : a ↔ b#print Iff
structure Iff (a b : Prop) : Prop
number of parameters: 2
fields:
  Iff.mp : a → b
  Iff.mpr : b → a
constructor:
  Iff.intro {a b : Prop} (mp : a → b) (mpr : b → a) : a ↔ b
theorem iff_sym (a b : Prop) (h : a ↔ b) : b ↔ a := a:Propb:Proph:a ↔ b⊢ b ↔ a a:Propb:Proph:a ↔ b⊢ b → aa:Propb:Proph:a ↔ b⊢ a → b a:Propb:Proph:a ↔ b⊢ b → a All goals completed! 🐙 a:Propb:Proph:a ↔ b⊢ a → b All goals completed! 🐙 theorem not_true_iff_false (b : Bool) : b ≠ true ↔ b = false := b:Bool⊢ b ≠ true ↔ b = false b:Bool⊢ b ≠ true → b = falseb:Bool⊢ b = false → b ≠ true b:Bool⊢ b ≠ true → b = false All goals completed! 🐙 b:Bool⊢ b = false → b ≠ true b:Boolh:b = false⊢ b ≠ true; b:Boolh:b = false⊢ false ≠ true; b:Boolh:b = falseh':false = true⊢ False; All goals completed! 🐙
Exercise★(iff_properties) (Optional)

Using the above proof that ↔ is symmetric (iff_sym) as a guide, prove that it is also reflexive and transitive.

theorem declaration uses `sorry`iff_refl (a : Prop) : a ↔ a := a:Prop⊢ a ↔ a All goals completed! 🐙 theorem declaration uses `sorry`iff_trans (a b c : Prop) (h₁ : a ↔ b) (h₂ : b ↔ c) : a ↔ c := a:Propb:Propc:Proph₁:a ↔ bh₂:b ↔ c⊢ a ↔ c All goals completed! 🐙
Exercise★★★(iff_practice)

Prove the following theorems about Iff:

theorem declaration uses `sorry`or_associate (a b c : Prop) : a ∨ (b ∨ c) ↔ (a ∨ b) ∨ c := a:Propb:Propc:Prop⊢ a ∨ b ∨ c ↔ (a ∨ b) ∨ c All goals completed! 🐙 theorem declaration uses `sorry`mul_eq_0 (n m : Nat) : n * m = 0 ↔ n = 0 ∨ m = 0 := n:Natm:Nat⊢ n * m = 0 ↔ n = 0 ∨ m = 0 All goals completed! 🐙 theorem declaration uses `sorry`or_distributes_over_and (a b c : Prop) : a ∨ (b ∧ c) ↔ (a ∨ b) ∧ (a ∨ c) := a:Propb:Propc:Prop⊢ a ∨ b ∧ c ↔ (a ∨ b) ∧ (a ∨ c) All goals completed! 🐙

8.2.6. Existential Quantification🔗

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.

Exists.{u} {α : Sort u} (p : α → Prop) : Prop#check Exists
Exists.{u} {α : Sort u} (p : α → Prop) : Prop
def Nat.Even x := ∃ n : Nat, x = Nat.double n Nat.Even : Nat → Prop#check (Nat.Even)
Nat.Even : Nat → Prop
open Nat in example : Even 4 := ⊢ Even 4 All 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.

example (n : Nat) : (∃ m, n = m + 4) → (∃ o, n = o + 2) := n:Nat⊢ (∃ m, n = m + 4) → ∃ o, n = o + 2 n:Natm:Nathm:n = m + 4⊢ ∃ o, n = o + 2 All goals completed! 🐙
Exercise★(dist_not_exists)

Prove that if a holds for all x, then there is no x for which a does not hold. (Hint: cases and obtain work on existential assumptions!)

theorem declaration uses `sorry`dist_not_exists (α : Type) (p : α → Prop) (h : ∀ x, p x) : ¬ (∃ x, ¬ p x) := α:Typep:α → Proph:∀ (x : α), p x⊢ ¬∃ x, ¬p x All goals completed! 🐙
Exercise★★(dist_exists_or)

Prove that existential quantification distributes over disjunction.

theorem declaration uses `sorry`dist_exists_or (α : Type) (p q : α → Prop) : (∃ x, p x ∨ q x) ↔ (∃ x, p x) ∨ (∃ x, q x) := α:Typep:α → Propq:α → Prop⊢ (∃ x, p x ∨ q x) ↔ (∃ x, p x) ∨ ∃ x, q x All goals completed! 🐙

8.3. Recap: Logical Connectives in Lean🔗

Connectives introduced in this chapter:

  • a ∧ b (conjunction):

    • introduced with constructor

    • 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.

8.4. Programming with Propositions🔗

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!

def List.In {α : Type} (x : α) (xs : List α) : Prop := match xs with | [] => False | x' :: xs' => x = x' ∨ In x xs' theorem List.In_nil {α : Type} {x : α} : ¬ (List.In x []) := α:Typex:α⊢ ¬In x [] α:Typex:α⊢ ¬False; α:Typex:αh:False⊢ False; All goals completed! 🐙 theorem List.In_cons {α : Type} {x x' : α} {xs : List α} : List.In x (x' :: xs) = (x = x' ∨ List.In x xs) := α:Typex:αx':αxs:List α⊢ In x (x' :: xs) = (x = x' ∨ In x xs) All goals completed! 🐙

When List.In is applied to a concrete list, it expands into a concrete sequence of nested disjunctions.

example : List.In 4 [1, 2, 3, 4, 5] := ⊢ List.In 4 [1, 2, 3, 4, 5] ⊢ 4 = 1 ∨ List.In 4 [2, 3, 4, 5]; ⊢ List.In 4 [2, 3, 4, 5]; ⊢ List.In 4 [3, 4, 5]; ⊢ List.In 4 [4, 5]; ⊢ 4 = 4; All goals completed! 🐙 example (n : Nat) (h : List.In n [2, 4]) : ∃ n' : Nat, n = 2 * n' := n:Nath:List.In n [2, 4]⊢ ∃ n', n = 2 * n' n:Nath:n = 2 ∨ List.In n [4]⊢ ∃ n', n = 2 * n' n:Nath:n = 2⊢ ∃ n', n = 2 * n'n:Nath:n = 4⊢ ∃ n', n = 2 * n'n:Nath:List.In n []⊢ ∃ n', n = 2 * n' n:Nath:n = 2⊢ ∃ n', n = 2 * n' All goals completed! 🐙 n:Nath:n = 4⊢ ∃ n', n = 2 * n' All goals completed! 🐙 n:Nath:List.In n []⊢ ∃ n', n = 2 * n' All goals completed! 🐙

We can also reason about more generic statements involving List.In.

theorem List.In_map {α β : Type} {f : α → β} {xs : List α} {x : α} (h : In x xs) : In (f x) (map f xs) := α:Typeβ:Typef:α → βxs:List αx:αh:In x xs⊢ In (f x) (map f xs) induction xs with α:Typeβ:Typef:α → βx:αh:In x []⊢ In (f x) (map f []) α:Typeβ:Typef:α → βx:αh:False⊢ In (f x) (map f []); All goals completed! 🐙 α:Typeβ:Typef:α → βx:αx':αxs':List αih:In x xs' → In (f x) (map f xs')h:In x (x' :: xs')⊢ In (f x) (map f (x' :: xs')) α:Typeβ:Typef:α → βx:αx':αxs':List αih:In x xs' → In (f x) (map f xs')h:x = x' ∨ In x xs'⊢ In (f x) (map f (x' :: xs')) α:Typeβ:Typef:α → βx:αx':αxs':List αih:In x xs' → In (f x) (map f xs')h:x = x'⊢ In (f x) (map f (x' :: xs'))α:Typeβ:Typef:α → βx:αx':αxs':List αih:In x xs' → In (f x) (map f xs')h:In x xs'⊢ In (f x) (map f (x' :: xs')) α:Typeβ:Typef:α → βx:αx':αxs':List αih:In x xs' → In (f x) (map f xs')h:x = x'⊢ In (f x) (map f (x' :: xs')) α:Typeβ:Typef:α → βx:αx':αxs':List αih:In x xs' → In (f x) (map f xs')h:x = x'⊢ f x' = f x' ∨ In (f x') (map f xs'); α:Typeβ:Typef:α → βx:αx':αxs':List αih:In x xs' → In (f x) (map f xs')h:x = x'⊢ f x' = f x'; All goals completed! 🐙 α:Typeβ:Typef:α → βx:αx':αxs':List αih:In x xs' → In (f x) (map f xs')h:In x xs'⊢ In (f x) (map f (x' :: xs')) α:Typeβ:Typef:α → βx:αx':αxs':List αih:In x xs' → In (f x) (map f xs')h:In x xs'⊢ f x = f x' ∨ In (f x) (map f xs'); α:Typeβ:Typef:α → βx:αx':αxs':List αih:In x xs' → In (f x) (map f xs')h:In x xs'⊢ In (f x) (map f xs'); All goals completed! 🐙

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.

Exercise★★(In_map_iff)
theorem declaration uses `sorry`List.In_map_iff {α β : Type} {f : α → β} {xs : List α} {y : β} : In y (map f xs) ↔ ∃ x, f x = y ∧ In x xs := α:Typeβ:Typef:α → βxs:List αy:β⊢ In y (map f xs) ↔ ∃ x, f x = y ∧ In x xs α:Typeβ:Typef:α → βxs:List αy:β⊢ In y (map f xs) → ∃ x, f x = y ∧ In x xsα:Typeβ:Typef:α → βxs:List αy:β⊢ (∃ x, f x = y ∧ In x xs) → In y (map f xs) α:Typeβ:Typef:α → βxs:List αy:β⊢ In y (map f xs) → ∃ x, f x = y ∧ In x xs All goals completed! 🐙 α:Typeβ:Typef:α → βxs:List αy:β⊢ (∃ x, f x = y ∧ In x xs) → In y (map f xs) All goals completed! 🐙
Exercise★★★(All)

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.)

def declaration uses `sorry`List.All {α : Type} (p : α → Prop) (l : List α) : Prop := sorry theorem declaration uses `sorry`List.All_nil {α : Type} {a : α → Prop} : List.All a [] := sorry theorem declaration uses `sorry`List.All_cons {α : Type} {p : α → Prop} {x : α} {l : List α} : List.All p (x :: l) = (p x ∧ All p l) := sorry theorem declaration uses `sorry`List.All_In {α : Type} {p : α → Prop} {l : List α} : (∀ x : α, In x l → p x) ↔ All p l := α:Typep:α → Propl:List α⊢ (∀ (x : α), In x l → p x) ↔ All p l All goals completed! 🐙
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.

def declaration uses `sorry`CombineOddEven (Odd Even : Nat → Prop) : Nat → Prop := sorry

To test your definition, prove the following facts:

theorem declaration uses `sorry`combineOddEven_intro (Odd Even : Nat → Prop) (n : Nat) (hOdd : Nat.odd n = true → Odd n) (hEven : Nat.odd n = false → Even n) : CombineOddEven Odd Even n := Odd:Nat → PropEven:Nat → Propn:NathOdd:n.odd = true → Odd nhEven:n.odd = false → Even n⊢ CombineOddEven Odd Even n All goals completed! 🐙 theorem declaration uses `sorry`combineOddEven_elim_odd (Odd Even : Nat → Prop) (n : Nat) (h : CombineOddEven Odd Even n) (hOdd : Nat.odd n = true) : Odd n := Odd:Nat → PropEven:Nat → Propn:Nath:CombineOddEven Odd Even nhOdd:n.odd = true⊢ Odd n All goals completed! 🐙 theorem declaration uses `sorry`combineOddEven_elim_even (Odd Even : Nat → Prop) (n : Nat) (h : CombineOddEven Odd Even n) (hOdd : Nat.odd n = false) : Even n := Odd:Nat → PropEven:Nat → Propn:Nath:CombineOddEven Odd Even nhOdd:n.odd = false⊢ Even n All goals completed! 🐙

8.5. Applying Theorems to Arguments🔗

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:

Nat.add_comm (n m : Nat) : n + m = m + n#check Nat.add_comm
Nat.add_comm (n m : Nat) : n + m = m + n
Nat.add_assoc (n m k : Nat) : n + m + k = n + (m + k)#check Nat.add_assoc
Nat.add_assoc (n m k : Nat) : n + m + k = n + (m + k)

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 ∀ n m : 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 ∀ n m : 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:

example (x y z : Nat) : x + (y + z) = (z + y) + x := unsolved goals x y z:Nat⊢ x + (y + z) = z + y + xx:Naty:Natz:Nat⊢ x + (y + z) = z + y + x x:Naty:Natz:Nat⊢ y + z + x = z + y + x x:Naty:Natz:Nat⊢ x + (y + z) = z + y + x
unsolved goals
x y z:Nat⊢ x + (y + z) = z + y + x

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.

example (x y z : Nat) : x + (y + z) = (z + y) + x := x:Naty:Natz:Nat⊢ x + (y + z) = z + y + x x:Naty:Natz:Nat⊢ y + z + x = z + y + x All goals completed! 🐙

If we really wanted, we could in fact do it for both rewrites.

example (x y z : Nat) : x + (y + z) = (z + y) + x := x:Naty:Natz:Nat⊢ x + (y + z) = z + y + x x:Naty:Natz:Nat⊢ y + z + x = z + y + x All goals completed! 🐙

The fact that implications are functions means we can prove them by explicitly providing a function.

theorem identity {a : Prop} : a → a := fun h => h
Quiz

Suppose 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 Nat n m 42 h₁ h₂
  1. n = m

  2. 42 = n

  3. n = 42

  4. Does not typecheck

Show solution
example (n m : Nat) (h₁ : n = m) (h₂ : m = 42) (trans_eq : ∀ {α : Type} {x y z : α}, x = y → y = z → x = z) : n = 42 := @trans_eq Nat n m 42 h₁ h₂
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₂
  1. n = m

  2. 42 = n

  3. n = 42

  4. Does not typecheck

Show solution
example (n m : Nat) (h₁ : n = m) (h₂ : m = 42) (trans_eq : ∀ {α : Type} {x y z : α}, x = y → y = z → x = z) : n = 42 := trans_eq h₁ h₂
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 Nat m 42 n h₂
  1. m = n

  2. m = n → 42 = n

  3. 42 = n → m = n

  4. Does not typecheck

Show solution
example (n m : 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} {x y z : α}, x = y → y = z → x = z) : 42 = n → m = n := @trans_eq Nat m 42 n h₂
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
  1. n = m → m = 42 → n = 42

  2. 42 = n → n = m → 42 = m

  3. n = 42 → 42 = m → n = m

  4. Does not typecheck

Show solution
example (n m : 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} {x y z : α}, x = y → y = z → x = z) : 42 = n → n = m → 42 = m := @trans_eq _ 42 n m
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₁
  1. m = n

  2. 42 = n

  3. n = 42

  4. Does not typecheck

Show solution
example (n m : Nat) (h₁ : n = m) (h₂ : m = 42) (trans_eq : ∀ {α : Type} {x y z : α}, x = y → y = z → x = z) : True := unsolved goals n m:Nath₁:n = mh₂:m = 42trans_eq:∀ {α : Type} {x y z : α}, x = y → y = z → x = z⊢ Truen:Natm:Nath₁:n = mh₂:m = 42trans_eq:∀ {α : Type} {x y z : α}, x = y → y = z → x = z⊢ True n:Natm:Nath₁:n = mh₂:m = 42trans_eq:∀ {α : Type} {x y z : α}, x = y → y = z → x = z⊢ True
Application type mismatch: The argument
  h₁
has type
  n = m
but is expected to have type
  42 = ?m.13
in the application
  trans_eq h₂ h₁

8.6. Working with Decidable Properties🔗

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...

example : Nat.even 42 = true := ⊢ Nat.even 42 = true All goals completed! 🐙

... or that there exists some k such that n = double k.

example : Nat.Even 42 := ⊢ Nat.Even 42 ⊢ ∃ n, 42 = n.double; All goals completed! 🐙

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!

To prove this, we first need two helper lemmas.

theorem even_double (k : Nat) : Nat.even (Nat.double k) = true := k:Nat⊢ k.double.even = true induction k with ⊢ (Nat.double 0).even = true ⊢ Nat.even 0 = true; All goals completed! 🐙 k':Natih:k'.double.even = true⊢ (k' + 1).double.even = true k':Natih:k'.double.even = true⊢ (k'.double + 2).even = true; All goals completed! 🐙
Exercise★★★(even_double_conv)
theorem declaration uses `sorry`even_double_conv (n : Nat) : ∃ k : Nat, n = bif Nat.even n then Nat.double k else Nat.double k + 1 := n:Nat⊢ ∃ k, n = bif n.even then k.double else k.double + 1 All goals completed! 🐙

Now the main theorem:

theorem Nat.even_bool_prop (n : Nat) : Nat.even n = true ↔ Even n := n:Nat⊢ n.even = true ↔ n.Even n:Nat⊢ n.even = true → n.Evenn:Nat⊢ n.Even → n.even = true n:Nat⊢ n.even = true → n.Even n:Nath:n.even = true⊢ n.Even n:Nath:n.even = truek:Nathk:n = bif n.even then k.double else k.double + 1⊢ n.Even n:Nath:n.even = truek:Nathk:n = bif true then k.double else k.double + 1⊢ n.Even; n:Nath:n.even = truek:Nathk:n = k.double⊢ n.Even; n:Nath:n.even = truek:Nathk:n = k.double⊢ ∃ n_1, n = n_1.double; All goals completed! 🐙 n:Nat⊢ n.Even → n.even = true n:Natk:Nathk:n = k.double⊢ n.even = true; n:Natk:Nathk:n = k.double⊢ k.double.even = true; All goals completed! 🐙

In view of this theorem, we can say that the boolean computation Nat.even n is reflected in the truth of the proposition ∃ k, n = Nat.double k.

Similarly, to state that two numbers n and m are equal, we can say either

  1. that n == m returns true, or

  2. that n = m.

Again, these two notions are equivalent.

theorem beq_eq_true (n m : Nat) : (n == m) = true ↔ n = m := n:Natm:Nat⊢ (n == m) = true ↔ n = m All goals completed! 🐙

(We use Nat.beq_eq_true_eq because n == m is a wrapper of DecidableEq Nat. 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.

def is_even_prime (n : Nat) : Bool := bif n == 2 then true else false

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.

example : Nat.Even 100 := ⊢ Nat.Even 100 All goals completed! 🐙

The proof of the corresponding boolean statement is simpler, because we don't have to invent the witness 50: computation does it for us!

example : Nat.even 100 = true := ⊢ Nat.even 100 = true All goals completed! 🐙

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:

example : Nat.Even 100 := ⊢ Nat.Even 100 h:Nat.even 100 = true → Nat.Even 100mpr✝:Nat.Even 100 → Nat.even 100 = true⊢ Nat.Even 100 h:Nat.even 100 = true → Nat.Even 100mpr✝:Nat.Even 100 → Nat.even 100 = true⊢ Nat.even 100 = true; All goals completed! 🐙

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.

example : Nat.even 101 = false := ⊢ Nat.even 101 = false All goals completed! 🐙

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.

example : ¬ Nat.Even 101 := ⊢ ¬Nat.Even 101 h:Nat.Even 101⊢ False; h:Nat.even 101 = true⊢ False h:Nat.even 99 = true⊢ False; All goals completed! 🐙

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.

theorem add_beq_true (n m p : Nat) (h : (n == m) = true) : (n + p == m + p) = true := n:Natm:Natp:Nath:(n == m) = true⊢ (n + p == m + p) = true n:Natm:Natp:Nath:n = m⊢ (n + p == m + p) = true All goals completed! 🐙

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.

theorem declaration uses `sorry`andb_true_iff (b1 b2 : Bool) : (b1 && b2) = true ↔ b1 = true ∧ b2 = true := b1:Boolb2:Bool⊢ (b1 && b2) = true ↔ b1 = true ∧ b2 = true All goals completed! 🐙 theorem declaration uses `sorry`orb_true_iff (b1 b2 : Bool) : (b1 || b2) = true ↔ b1 = true ∨ b2 = true := b1:Boolb2:Bool⊢ (b1 || b2) = true ↔ b1 = true ∨ b2 = true All goals completed! 🐙
Exercise★★★(beqList)

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.

def declaration uses `sorry`beqList {α : Type} (beq : α → α → Bool) (xs ys : List α) : Bool := sorry theorem declaration uses `sorry`beqList_nil_nil {α : Type} {beq : α → α → Bool} : beqList beq [] [] = true := sorry theorem declaration uses `sorry`beqList_cons_cons {α : Type} {beq : α → α → Bool} {x y : α} {xs ys : List α} : beqList beq (x :: xs) (y :: ys) = (beq x y && beqList beq xs ys) := sorry theorem declaration uses `sorry`beqList_nil_cons {α : Type} {beq : α → α → Bool} {x : α} {xs : List α} : beqList beq [] (x :: xs) = false := sorry theorem declaration uses `sorry`beqList_cons_nil {α : Type} {beq : α → α → Bool} {x : α} {xs : List α} : beqList beq (x :: xs) [] = false := sorry theorem declaration uses `sorry`beqList_true_iff α (beq : α → α → Bool) (h : ∀ (x y : α), beq x y = true ↔ x = y) : ∀ {xs ys : List α}, beqList beq xs ys = true ↔ xs = ys := α:Typebeq:α → α → Boolh:∀ (x y : α), beq x y = true ↔ x = y⊢ ∀ {xs ys : List α}, beqList beq xs ys = true ↔ xs = ys All goals completed! 🐙
Exercise★★(List.allb)

Prove the theorem below, which relates List.allb, from the exercise Tactics.forall_exists_challenge, to the List.All property defined above.

Copy the definition of List.allb from Tactics here so that this file can be graded on its own.

def declaration uses `sorry`List.allb {α : Type} (test : α → Bool) (l : List α) : Bool := sorry theorem declaration uses `sorry`List.allb_nil {α : Type} {test : α → Bool} : allb test [] = true := sorry theorem declaration uses `sorry`List.allb_cons {α : Type} {test : α → Bool} {x : α} {l : List α} : allb test (x :: l) = (test x && allb test l) := sorry theorem declaration uses `sorry`List.allb_true_iff α {test : α → Bool} {l : List α} : allb test l = true ↔ All (fun x => test x = true) l := α:Typetest:α → Booll:List α⊢ allb test l = true ↔ All (fun x => test x = true) l All goals completed! 🐙

(Ungraded thought question) Are there any important properties of the function List.allb that are not captured by this specification?

8.7. The Logic of Lean🔗

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.

8.7.1. Propositional Extensionality🔗

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:

∀ (a b : Prop), (a ∧ b) = (b ∧ a) : Prop#check (∀ a b : Prop, (a ∧ b) = (b ∧ a) : Prop)

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 (a b : Prop) : a ∧ b = b ∧ a := a:Propb:Prop⊢ a ∧ b = b ∧ a Tactic `rfl` failed: The left-hand side a is not definitionally equal to the right-hand side b = b ∧ a a b:Prop⊢ a ∧ b = b ∧ aa:Propb:Prop⊢ a ∧ b = b ∧ a
Tactic `rfl` failed: The left-hand side
  a
is not definitionally equal to the right-hand side
  b = b ∧ a

a b:Prop⊢ a ∧ b = b ∧ a
example (a b : Prop) : a ∧ b = b ∧ a := a:Propb:Prop⊢ a ∧ b = b ∧ a Tactic `cases` failed: major premise type is not an inductive type Prop Explanation: 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. a b:Prop⊢ a ∧ b = b ∧ aa:Propb:Prop⊢ a ∧ b = b ∧ a
Tactic `cases` failed: major premise type is not an inductive type
  Prop

Explanation: 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.

a b: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 {a b : Prop} : a ∧ b ↔ b ∧ a#check and_comm
and_comm {a b : 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).

axiom propext : ∀ {a b : Prop}, (a ↔ b) → a = b#print propext
axiom propext : ∀ {a b : 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:

theorem prop_true (a : Prop) (h : a) : a = True := a:Proph:a⊢ a = True a:Proph:a⊢ a ↔ True a:Proph:a⊢ a → Truea:Proph:a⊢ True → a a:Proph:a⊢ a → True a:Proph:aa✝:a⊢ True All goals completed! 🐙 a:Proph:a⊢ True → a a:Proph:aa✝:True⊢ a All goals completed! 🐙

Lean provides an ext tactic that applies propext for us. We can use it to show that commuted conjoined propositions are equal.

theorem and_comm_eq (a b : Prop) : (a ∧ b) = (b ∧ a) := a:Propb:Prop⊢ (a ∧ b) = (b ∧ a) a:Propb:Prop⊢ a ∧ b ↔ b ∧ a; All goals completed! 🐙

Similarly, we can use it to show that reassociated conjoined propositions are equal as well.

and_assoc {a b c : Prop} : (a ∧ b) ∧ c ↔ a ∧ b ∧ c#check and_assoc
and_assoc {a b c : Prop} : (a ∧ b) ∧ c ↔ a ∧ b ∧ c
theorem and_assoc_eq (a b c : Prop) : ((a ∧ b) ∧ c) = (a ∧ (b ∧ c)) := a:Propb:Propc:Prop⊢ ((a ∧ b) ∧ c) = (a ∧ b ∧ c) a:Propb:Propc:Prop⊢ (a ∧ b) ∧ c ↔ a ∧ b ∧ c; All goals completed! 🐙

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.

theorem and_comm_flip (a b c : Prop) : (a ∧ b ∧ c) ↔ (c ∧ b ∧ a) := a:Propb:Propc:Prop⊢ a ∧ b ∧ c ↔ c ∧ b ∧ a All goals completed! 🐙

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.

theorem and_comm_flip' (a b c : Prop) : (a ∧ b ∧ c) ↔ (c ∧ b ∧ a) := a:Propb:Propc:Prop⊢ a ∧ b ∧ c ↔ c ∧ b ∧ a All goals completed! 🐙

Under the hood, this proof still uses propext, which you can check by asking for all of the axioms used by a declaration.

'and_comm_flip' depends on axioms: [propext]#print axioms and_comm_flip
'and_comm_flip' depends on axioms: [propext]
'and_comm_flip'' depends on axioms: [propext]#print axioms and_comm_flip'
'and_comm_flip'' depends on axioms: [propext]
Exercise★(mul_eq_0_ternary)
theorem declaration uses `sorry`mul_eq_0_ternary (n m p : Nat) : n * m * p = 0 ↔ n = 0 ∨ m = 0 ∨ p = 0 := n:Natm:Natp:Nat⊢ n * m * p = 0 ↔ n = 0 ∨ m = 0 ∨ p = 0 All goals completed! 🐙
Exercise★★(In_append_iff)
theorem declaration uses `sorry`In_append_iff (α : Type) (l l' : List α) (x : α) : List.In x (l ++ l') ↔ List.In x l ∨ List.In x l' := α:Typel:List αl':List αx:α⊢ List.In x (l ++ l') ↔ List.In x l ∨ List.In x l' All goals completed! 🐙
Exercise★(beq_neq_false)

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.

theorem declaration uses `sorry`beq_neq_false (n m : Nat) : (n == m) = false ↔ n ≠ m := n:Natm:Nat⊢ (n == m) = false ↔ n ≠ m All goals completed! 🐙

8.7.2. Functional Extensionality🔗

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 : (fun x => x + 2) = (fun x => x + (Nat.pred 3)) := ⊢ (fun x => x + 2) = fun x => x + Nat.pred 3 All goals completed! 🐙

But this doesn't always work the way we'd like:

example : (fun x => x + 2) = (fun x => 2 + x) := ⊢ (fun x => x + 2) = fun x => 2 + x Tactic `rfl` failed: The left-hand side fun x => x + 2 is not definitionally equal to the right-hand side fun x => 2 + x ⊢ (fun x => x + 2) = fun x => 2 + x⊢ (fun x => x + 2) = fun x => 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.

fun {α β} f g => funext : ∀ {α β : Type} (f g : α → β), (∀ (x : α), f x = g x) → f = g#check (fun f g => funext (f := f) (g := g) : ∀ {α β : Type} (f g : α → β), (∀ x, f x = g x) → f = g)

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]#print axioms funext
'funext' depends on axioms: [Quot.sound]

Now we can prove some intuitively obvious equalities about functions that would not be provable without funext.

theorem add_comm_fun : (fun (n m : Nat) => n + m) = (fun (n m : Nat) => m + n) := ⊢ (fun n m => n + m) = fun n m => m + n ⊢ ∀ (x : Nat), (fun m => x + m) = fun m => m + x; n:Nat⊢ (fun m => n + m) = fun m => m + n n:Nat⊢ ∀ (x : Nat), n + x = x + n; n:Natm:Nat⊢ n + m = m + n All goals completed! 🐙

The ext tactic will also apply funext as many times as possible, introducing all variables in one go. The singular version of the tactic is ext1.

theorem add_comm_fun' : (fun (n m : Nat) => n + m) = (fun (n m : Nat) => m + n) := ⊢ (fun n m => n + m) = fun n m => m + n n:Natm:Nat⊢ n + m = m + n; All goals completed! 🐙
Quiz

Is the following statement provable by just rfl, without funext?

(fun xs => 1 :: xs) = (fun xs => [1] ++ xs)
  1. Yes

  2. No

Show solution
example : (fun xs => 1 :: xs) = (fun xs => [1] ++ xs) := ⊢ (fun xs => 1 :: xs) = fun xs => [1] ++ xs All goals completed! 🐙

8.7.3. Other Extensionality Principles🔗

Functions and propositions are not the only things that have extensionality principles. Many structures like pairs also have them:

example {n : Nat} {p : Nat × Nat} (hx_fst : p.fst = n + 1) (hx_snd : p.snd = 0) : (n + 1, 0) = p := n:Natp:Nat × Nathx_fst:p.fst = n + 1hx_snd:p.snd = 0⊢ (n + 1, 0) = p n:Natp:Nat × Nathx_fst:p.fst = n + 1hx_snd:p.snd = 0⊢ (n + 1, 0).fst = p.fstn:Natp:Nat × Nathx_fst:p.fst = n + 1hx_snd:p.snd = 0⊢ (n + 1, 0).snd = p.snd -- uses the `Prod.ext` lemma n:Natp:Nat × Nathx_fst:p.fst = n + 1hx_snd:p.snd = 0⊢ (n + 1, 0).fst = p.fst All goals completed! 🐙 n:Natp:Nat × Nathx_fst:p.fst = n + 1hx_snd:p.snd = 0⊢ (n + 1, 0).snd = p.snd All goals completed! 🐙
Exercise★★(prod_ext_example)

Now, use ext1 to prove the following. Remember that dsimp only simplifies projections like (a, b).fst to a.

theorem declaration uses `sorry`prod_ext_example {m : Nat} {p : Nat × Nat} (hp_snd : p.snd = 4) (hp_fst : p.fst = m) : ((p.fst + 1, 2), (p.fst, 4)) = ((m + 1, p.snd - 2), p) := m:Natp:Nat × Nathp_snd:p.snd = 4hp_fst:p.fst = m⊢ ((p.fst + 1, 2), p.fst, 4) = ((m + 1, p.snd - 2), p) All goals completed! 🐙
Exercise★★★★(trRev_correct)

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:

def revAppend {α} (xs ys : List α) : List α := match xs with | [] => ys | x :: xs => revAppend xs (x :: ys) theorem revAppend_nil {α : Type} {xs : List α} : revAppend [] xs = xs := α:Typexs:List α⊢ revAppend [] xs = xs All goals completed! 🐙 theorem revAppend_cons {α : Type} {x : α} {xs ys : List α} : revAppend (x :: xs) ys = revAppend xs (x :: ys) := α:Typex:αxs:List αys:List α⊢ revAppend (x :: xs) ys = revAppend xs (x :: ys) All goals completed! 🐙 def trRev {α} (xs : List α) : List α := revAppend xs []

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.

-- FILL IN HERE -- FILL IN HERE theorem declaration uses `sorry`trRev_correct {α : Type} : @trRev α = @List.rev α := α:Type⊢ trRev = List.rev All goals completed! 🐙

8.7.4. Classical vs. Constructive Logic🔗

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:

def ExcludedMiddle := ∀ 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.

theorem restricted_excluded_middle (a : Prop) (b : Bool) (h : a ↔ b = true) : a ∨ ¬ a := a:Propb:Boolh:a ↔ b = true⊢ a ∨ ¬a cases b with a:Proph:a ↔ false = true⊢ a ∨ ¬a a:Proph:a ↔ false = true⊢ ¬a; a:Proph:a ↔ false = true⊢ ¬false = true; a:Proph:a ↔ false = truea✝:false = true⊢ False; All goals completed! 🐙 a:Proph:a ↔ true = true⊢ a ∨ ¬a a:Proph:a ↔ true = true⊢ a; All goals completed! 🐙

In particular, the excluded middle is valid for equations n = m between natural numbers n and m.

theorem excluded_middle_nat_eq (n m : Nat) : n = m ∨ n ≠ m := n:Natm:Nat⊢ n = m ∨ n ≠ m n:Natm:Nat⊢ n = m ↔ (n == m) = true n:Natm:Nat⊢ (n == m) = true ↔ n = m; All goals completed! 🐙

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#check Classical.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.

axiom Classical.choice.{u} : {α : Sort u} → Nonempty α → α#print Classical.choice
axiom Classical.choice.{u} : {α : Sort u} → Nonempty α → α
'Classical.em' depends on axioms: [propext, Classical.choice, Quot.sound]#print axioms Classical.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.

theorem em : ∀ a, a ∨ ¬ a := ⊢ ∀ (a : Prop), a ∨ ¬a a:Prop⊢ a ∨ ¬a a:Proph:a⊢ a ∨ ¬aa:Proph:¬a⊢ a ∨ ¬a /- h : a -/ a:Proph:a⊢ a ∨ ¬a a:Proph:a⊢ a; All goals completed! 🐙 /- h : ¬ a -/ a:Proph:¬a⊢ a ∨ ¬a a:Proph:¬a⊢ ¬a; All goals completed! 🐙 'em' depends on axioms: [propext, Classical.choice, Quot.sound]#print axioms em
'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#check Classical.not_not 'Classical.not_not' depends on axioms: [propext, Classical.choice, Quot.sound]#print axioms Classical.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.

theorem declaration uses `sorry`excluded_middle_irrefutable (a : Prop) : ¬ ¬ (a ∨ ¬ a) := a:Prop⊢ ¬¬(a ∨ ¬a) All goals completed! 🐙
Exercise★★★(not_exists_dist) (Advanced)

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.

theorem declaration uses `sorry`not_exists_dist (α : Type) (p : α → Prop) : (¬ ∃ x : α, ¬ p x) → (∀ x : α, p x) := α:Typep:α → Prop⊢ (¬∃ x, ¬p x) → ∀ (x : α), p x All goals completed! 🐙
Exercise★★★★★(classical_axioms) (Optional)

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.

def Peirce := ∀ a b : Prop, ((a → b) → a) → a def NotNot := ∀ a : Prop, ¬ ¬ a → a def DeMorganNotAndNot := ∀ a b : Prop, ¬ (¬ a ∧ ¬ b) → a ∨ b def ImpOr := ∀ a b : Prop, (a → b) → (¬ a ∨ b) def ConsequentiaMirabilis := ∀ a : Prop, (¬ a → a) → a theorem declaration uses `sorry`ImpOr_em : ImpOr → ExcludedMiddle := ⊢ ImpOr → ExcludedMiddle All goals completed! 🐙 theorem declaration uses `sorry`em_ImpOr : ExcludedMiddle → ImpOr := ⊢ ExcludedMiddle → ImpOr All goals completed! 🐙 theorem declaration uses `sorry`em_demorgan : ExcludedMiddle → DeMorganNotAndNot := ⊢ ExcludedMiddle → DeMorganNotAndNot All goals completed! 🐙 theorem declaration uses `sorry`demorgan_em : DeMorganNotAndNot → ExcludedMiddle := ⊢ DeMorganNotAndNot → ExcludedMiddle All goals completed! 🐙 theorem declaration uses `sorry`em_not_not : ExcludedMiddle → NotNot := ⊢ ExcludedMiddle → NotNot All goals completed! 🐙 theorem declaration uses `sorry`not_not_em' : NotNot → ExcludedMiddle := ⊢ NotNot → ExcludedMiddle All goals completed! 🐙 theorem declaration uses `sorry`em_cm : ExcludedMiddle → ConsequentiaMirabilis := ⊢ ExcludedMiddle → ConsequentiaMirabilis All goals completed! 🐙 theorem declaration uses `sorry`cm_em : ConsequentiaMirabilis → ExcludedMiddle := ⊢ ConsequentiaMirabilis → ExcludedMiddle All goals completed! 🐙 theorem declaration uses `sorry`cm_not_not : ConsequentiaMirabilis → NotNot := ⊢ ConsequentiaMirabilis → NotNot All goals completed! 🐙 theorem declaration uses `sorry`not_not_cm : NotNot → ConsequentiaMirabilis := ⊢ NotNot → ConsequentiaMirabilis All goals completed! 🐙 theorem declaration uses `sorry`cm_peirce : ConsequentiaMirabilis → Peirce := ⊢ ConsequentiaMirabilis → Peirce All goals completed! 🐙 theorem declaration uses `sorry`peirce_cm : Peirce → ConsequentiaMirabilis := ⊢ Peirce → ConsequentiaMirabilis All goals completed! 🐙
Source revision: e85fe77, committed 2026-10-06 21:16 UTC