Logical Foundations

8. Logic in Lean🔗

Note to developers (before next release)

Unlike earlier chapters, there are probably too many WORKINCLASSes in this chapter. BCP 20: But conversely some more quizzes would be great!

import LF.Basics
import LF.Induction
import LF.Poly
import LF.Tactics
import LF.CustomTactics
Note to developers (Mike Hicks @mwhicks1)

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.

8.1. The Prop Type🔗

So far, we have seen:

  • propositions: mathematical statements, so far only of three kinds:

    • equality propositions (e1 = e2)

    • implications (a → b)

    • quantified propositions (∀ x, a)

  • proofs: ways of presenting evidence for the truth of a proposition

In this chapter we will introduce several more flavors of both propositions and proofs.

Like everything in Lean, well-formed propositions have a type:

∀ (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)

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! 🐙

Propositions are first-class entities. For example, we can name them:

def PlusClaim : Prop := 2 + 2 = 4 PlusClaim : Prop#check PlusClaim
PlusClaim : Prop
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.

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! 🐙
Note to developers (Mike Hicks @mwhicks1)

Is it confusing that you can do intro through the Injective definition? Is it worth a word about that? Have students seen this happen to this point?

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

The injectivity/disjointness principles from the Tactics chapter apply to equality hypotheses too, and cases can exploit them directly:

-- 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! 🐙 -- 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! 🐙

There are more examples of this kind of reasoning yet to come.

As a convenience, Lean will cast booleans to propositions by equating them to true, which is why checking them against Prop succeeds. For clarity, we will generally avoid relying on these implicit casts.

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.

declaration uses `sorry`example (n m : Nat) : n = 0 ∧ m = 0 → n + m = 0 := n:Natm:Nat⊢ n = 0 ∧ 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! 🐙

For the present example, both ways work. But in other situations, we may wind up with a conjunctive hypothesis in the middle of a proof...

declaration uses `sorry`example (n m : Nat) (h : n + m = 0) : n * m = 0 := n:Natm:Nath:n + m = 0⊢ n * m = 0 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! 🐙

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 declaration uses `sorry`Nat.zero_or_succ (n : Nat) : n = 0 ∨ n = (n - 1).succ := n:Nat⊢ n = 0 ∨ n = (n - 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! 🐙

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 declaration uses `sorry`contradiction_implies_anything (a b : Prop) (h : a ∧ ¬ a) : b := a:Propb:Proph:a ∧ ¬a⊢ b All goals completed! 🐙 theorem declaration uses `sorry`double_neg (a : Prop) (ha : a) : ¬ ¬ a := a:Propha:a⊢ ¬¬a All goals completed! 🐙

Since inequality involves a negation, getting comfortable with it also often requires a little practice.

A useful trick: if you are trying to prove a nonsensical goal, apply ex_falso_quodlibet to change the goal to False. This makes it easier to use assumptions of the form ¬ a, and in particular of the form x ≠ y.

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

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.

You can use Iff.mp to access the forward direction of the iff and Iff.mpr to access the backwards direction — these eliminate an iff — and Iff.intro to convert a goal of the form a ↔ b to two goals of the form a → b and b → a, which introduces an iff.

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 declaration uses `sorry`iff_sym (a b : Prop) (h : a ↔ b) : b ↔ a := a:Propb:Proph:a ↔ b⊢ b ↔ a 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! 🐙

8.2.6. Existential Quantification🔗

Note to developers (Mike Hicks @mwhicks1)

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)
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! 🐙

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🔗

What does it mean to say that "an element x occurs in a list l"?

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

declaration uses `sorry`example : List.In 4 [1, 2, 3, 4, 5] := ⊢ List.In 4 [1, 2, 3, 4, 5] All goals completed! 🐙 declaration uses `sorry`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' 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! 🐙

8.5. Applying Theorems to Arguments🔗

Lean also treats proofs as first-class objects!

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.

Lean actually allows us to apply a theorem as if it were a function. This is often handy in proof scripts — e.g., suppose we want to prove the following:

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! 🐙

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 namespace FunctionTheoremQuiz
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₁
end FunctionTheoremQuiz

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     |

Since functions in Lean by default must terminate on all inputs, a terminating function of type Nat → Bool is a decision procedure — i.e., it yields true or false on all inputs.

For example, Nat.even is a decision procedure for the property "is even".

Since Prop includes both decidable and undecidable properties, we have two options when we want to formalize a property that happens to be decidable: we can express it either as a boolean computation, or as a function into Prop.

For instance, to claim that a number n is even, we can say either that Nat.even n evaluates to true...

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! 🐙
Note to developers (Yipeng Liu @berberman)

Same issue as CombineOddEven.

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

An important benefit of stating facts using booleans is enabling some proof automation through computation with terms, a technique known as proof by reflection.

Consider the following statement:

Nat.Even 100

The most direct way to prove this is to give the value of k explicitly.

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! 🐙
Note to developers (Mike Hicks @mwhicks1)

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:

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.

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.

declaration uses `sorry`example : ¬ Nat.Even 101 := ⊢ ¬Nat.Even 101 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 declaration uses `sorry`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 All goals completed! 🐙

8.7. The Logic of Lean🔗

Lean's logical core is a "metalanguage for mathematics" in the same sense as familiar foundations for paper-and-pencil math, like Zermelo–Fraenkel Set Theory (ZFC).

Mostly, the differences are not too important, but a few points are useful to understand.

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.

A first instance has to do with equality of propositions. For example:

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

Unfortunately, we cannot prove this equality directly.

However, we can prove that a ∧ b implies b ∧ a, and vice versa — this is the commutativity of conjunction that we have seen earlier.

and_comm {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

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.

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! 🐙

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)

Technically, funext is not an axiom, but its proof depends on one (which we will not explain).

'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🔗

We can use ext on pairs as follows:

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! 🐙

8.7.4. Classical vs. Constructive Logic🔗

The following reasoning principle is not derivable with the tools we've seen so far:

def ExcludedMiddle := ∀ a : Prop, a ∨ ¬ a

Logical systems in which excluded middle does not hold are referred to as constructive logics. They are so called because to prove a proposition, we must give a construction for it; for instance, ∃ x, p x is proven by providing a particular value of x.

Logical systems in which excluded middle does hold, such as ZFC set theory, are referred to as classical.

Both variants, classical and constructive, are examples of first-order logic: propositions are built from a fixed stock of connectives (∧, ∨, ¬, →, ↔) and quantifiers (∀, ∃) that range over the individual elements of some domain (natural numbers, lists, and so on), but never over propositions or predicates themselves. Classical first-order logic — first-order logic together with excluded middle — is the logic usually taught in an introductory logic course, and it underlies foundations like ZFC.

Lean's own logic goes further than this, because propositions are themselves Lean terms of type Prop, so we can quantify over them directly. ExcludedMiddle above, ∀ a : Prop, a ∨ ¬ a, does exactly that: it quantifies over all propositions, not over the elements of some fixed domain. Logics that allow quantifying over propositions or predicates, rather than only over individuals, are called higher-order; Lean's logic is a higher-order one, of which first-order logic is a fragment.

Lean provides classical reasoning principles in the Classical library, including excluded middle.

Classical.em (p : Prop) : p ∨ ¬p#check Classical.em
Classical.em (p : Prop) : p ∨ ¬p
Source revision: e85fe77, committed 2026-10-06 21:16 UTC