import LeanFoundations.Logic.IndProp

Proof Objects #

"Algorithms are the computational content of proofs." —Robert Harper

We have seen that Lean has mechanisms both for programming, using inductive and recursive definitions, and for proving, using inductive types, recursive functions, tactics, etc. So far, we have mostly treated these mechanisms as if they were quite separate, and for many purposes this is a good way to think. But we have also seen hints that Lean's programming and proving facilities are closely related. For example, the keyword inductive is used to declare both datatypes and propositions, and is used both to describe the type of functions on data and logical implication. This is not just a syntactic accident! In fact, programs and proofs in Lean are almost the same thing. In this chapter, we will study this connection more closely.

We have already seen the fundamental idea: provability in Lean is represented by concrete evidence. When we construct a proof of a basic proposition, we are actually building a tree of evidence, where each node in the tree is an instance of an inference rule, and the children of each node are the subproofs of the premises of that rule. If the premise is an assumption, then the corresponding leaf in the evidence tree is the assumption's label.

For example, consider the following simple proof:

example (n : Nat) : n = n := by rfl

This proof can also be written as:

example (n : Nat) : n = n := Eq.refl n

In the second version, we have explicitly provided the proof object Eq.refl n of type n = n.

Proof Scripts vs. Proof Objects #

We have been writing proof scripts, i.e., sequences of tactics that instruct Lean how to build up a proof object behind the scenes. Alternatively, we can write out this proof object directly, without tactics:

theorem plus_1_neq_0_firsttry :  n : Nat, n + 1  0 := by
  intro n
  intro H
  cases H

This is the same as:

theorem plus_1_neq_0_secondtry :  n : Nat, n + 1  0 :=
  fun n => fun H => Nat.noConfusion H

Or even:

theorem plus_1_neq_0_thirdtry :  n : Nat, n + 1  0 :=
  fun n H => Nat.noConfusion H

You can see that proof scripts and proof objects serve the same purpose — they carry out the construction of evidence for the truth of propositions. Proof scripts are easier to work with because they give us the power of tactics. But proof objects are often more illuminating, because they make explicit the logical structure of the proof.

Quantifiers, Implications, Functions #

In Lean's computational universe (where data structures and programs live), there are two sorts of values with arrows in their types: constructors introduced by inductively defined data types, and functions.

Similarly, in Lean's logical universe (where we carry out proofs), there are two ways of giving evidence for an implication: constructors introduced by inductively defined propositions, and... functions!

For example, consider this statement:

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

In principle, we could prove this by providing evidence for each possible pair of naturals, but this would be impossible to finish — there are infinitely many pairs!

The universal quantifier (and implication , which is just a special case) is built into Lean's logic in a fundamental way: what we mean when we say "∀ n : Nat, P n" is that we can supply a proof of P n for any specific choice of n. In other words, a proof of ∀ n : Nat, P n is a function that takes any natural number n and returns a proof of P n.

In the above statement, for example, n and m are bound by universal quantifiers, so a proof would be a function that takes two natural numbers and returns evidence for the corresponding instance of the equality n + m = m + n.

theorem plus_comm :  n m : Nat, n + m = m + n := by
  intro n m
  induction n with
  | zero => rw [Nat.zero_add, Nat.add_zero]
  | succ n' ih => rw [Nat.succ_add, Nat.add_succ, ih]

The same proof can be written directly as a function:

theorem plus_comm' :  n m : Nat, n + m = m + n :=
  fun n m => by
    induction n with
    | zero => rw [Nat.zero_add, Nat.add_zero]
    | succ n' ih => rw [Nat.succ_add, Nat.add_succ, ih]

In general, P → Q is just syntactic sugar for ∀ (_ : P), Q. That is, they mean the same thing: both describe functions that take evidence for P and produce evidence for Q.

For example:

theorem plus_1_neq_0 :  n : Nat, n + 1  0 :=
  fun n H => Nat.noConfusion H

theorem plus_1_neq_0' (n : Nat) : n + 1  0 :=
  fun H => Nat.noConfusion H

Both have the same meaning: they are functions that take a natural number n and a hypothesis H stating that n + 1 = 0, and return a proof of False (since n + 1 is never 0).

Programming with Tactics vs. Defining Functions #

If the idea of "proof objects" is unfamiliar, let me point out that we use proof objects all the time in informal mathematics. For example, here is a textbook proof that addition is associative:

Theorem: Addition is associative. Proof: By induction on n.

This is essentially the same as the following proof object:

theorem plus_assoc :  n m p : Nat, n + (m + p) = (n + m) + p := by
  intro n m p
  exact (Nat.add_assoc n m p).symm

Or more directly:

theorem plus_assoc' :  n m p : Nat, n + (m + p) = (n + m) + p :=
  fun n m p => (Nat.add_assoc n m p).symm

Logical Connectives as Inductive Types #

Inductive definitions are powerful enough to express most of the logical connectives we have seen so far. Indeed, only universal quantification (with implication as a special case) is built into Lean; all the others — conjunction, disjunction, negation, existential quantification, even equality — can be encoded using inductive definitions. Let's see how.

Conjunction #

To prove that P ∧ Q holds, we must present evidence for both P and Q. Thus, it makes sense to define a proof object for P ∧ Q as consisting of a pair of two proofs: one for P and another for Q. This leads to the following definition.

namespace MyLogic
inductive And (A B : Prop) : Prop where
  | intro (a : A) (b : B) : And A B

Notice the similarity with the definition of the prod type, given in chapter Poly; the only difference is that prod takes Type arguments, whereas And takes Prop arguments.

#print And.intro
#check @And.intro

This explains the constructor tactic: to prove a goal that is a conjunction, apply the single constructor of the And inductive family.

Exercise: 1 star, standard, optional (proj1) #

Here's a theorem that says: if P ∧ Q is true, then P is true.

theorem proj1 :  P Q : Prop, And P Q  P := by
  intro P Q H
  cases H with
  | intro hp hq => exact hp

Exercise: 1 star, standard, optional (proj2) #

Similarly, here's a theorem that says: if P ∧ Q is true, then Q is true.

theorem proj2 :  P Q : Prop, And P Q  Q := by
  intro P Q H
  cases H with
  | intro hp hq => exact hq

Exercise: 1 star, standard, optional (and_comm) #

Theorem: P ∧ Q implies Q ∧ P.

theorem and_comm :  P Q : Prop, And P Q  And Q P := by
  intro P Q H
  cases H with
  | intro hp hq => exact And.intro hq hp

This shows why the cases tactic can be used to reason about conjunction: it is defined as an inductive type with one constructor, and cases simply applies the inverse of the constructor.

Disjunction #

The inductive definition of disjunction uses two constructors, one for each side of the disjunct:

inductive Or (A B : Prop) : Prop where
  | inl (a : A) : Or A B
  | inr (b : B) : Or A B

This explains the behavior of the left and right tactics: they are simply applications of the first and second constructors of Or. It also explains the behavior of the cases tactic on a disjunctive hypothesis, since the inversion of the constructors generates two subgoals.

Exercise: 1 star, standard, optional (or_comm) #

Theorem: P ∨ Q implies Q ∨ P.

theorem or_comm :  P Q : Prop, Or P Q  Or Q P := by
  intro P Q H
  cases H with
  | inl hp => exact Or.inr hp
  | inr hq => exact Or.inl hq

Existential Quantification #

To give evidence for an existential quantifier, we package a witness together with a proof that the witness satisfies the property in question.

inductive Ex {A : Type} (P : A  Prop) : Prop where
  | intro (witness : A) (proof : P witness) : Ex P

This may benefit from a little unpacking. The core definition is for a type former Ex that can be used to build propositions of the form Ex P, where P itself is a function from witness values in the type A to propositions. The Ex.intro constructor then offers a way of constructing evidence for Ex P, given a witness value and a proof that the witness satisfies the property P.

The more familiar form ∃ x, P x desugars to an expression involving Ex:

#check ( n : Nat, n + n = 4)
#check (Ex (fun n : Nat => n + n = 4))

Here's how to define the familiar introduction construct for existentials:

theorem some_nat_is_even :  n : Nat, n + n = 4 :=
  2, rfl

Exercise: 1 star, standard, optional (ex_ev_Sn) #

Complete the definition of the following proof object:

theorem ex_ev_Sn :  n : Nat, Even (n + 1) := by
  sorry

True and False #

The inductive definition of the True proposition is simple: it is defined as an inductive type with a single constructor.

inductive True : Prop where
  | intro : True

It has one constructor (so every proof of True is the same, so being given a proof of True is not informative.)

False is equally simple — indeed, so simple it may look syntactically wrong at first glance!

inductive False : Prop where

That is, False is an inductive type with no constructors — i.e., no way to build evidence for it.

Exercise: 1 star, standard, optional (False_ind) #

How does the exfalso tactic work? It applies the following theorem:

theorem False_ind :  P : Prop, False  P := by
  intro P H
  cases H

Equality #

Even Lean's equality relation is not built in. It has the following inductive definition. (Actually, the definition in the Lean standard library is a bit different for technical reasons, but this is equivalent.)

inductive Eq' {A : Type} (x : A) : A  Prop where
  | refl : Eq' x x

The way to think about this definition is that, given a set A, it defines a family of propositions "x is equal to y," indexed by pairs of values (x and y) from A. There is just one way to construct evidence for members of this family: applying the constructor refl to show that every value is equal to itself.

Exercise: 2 stars, standard, optional (equality__leibniz_equality) #

The inductive definition of equality corresponds to Leibniz equality: what we mean when we say "x and y are equal" is that every property on one side of the equation also holds on the other side. This is captured by the following theorem:

theorem equality__leibniz_equality :  (X : Type) (x y : X),
  Eq' x y   P : X  Prop, P x  P y := by
  intro X x y H P Hx
  cases H
  exact Hx

Exercise: 2 stars, standard, optional (leibniz_equality__equality) #

Show that, in fact, the converse is true:

theorem leibniz_equality__equality :  (X : Type) (x y : X),
  ( P : X  Prop, P x  P y)  Eq' x y := by
  intro X x y H
  apply H
  apply Eq'.refl

end MyLogic

Inversion, Again #

We've seen that tactics like cases and induction can be applied to evidence for inductively defined propositions, not just to evidence for inductively defined data. This should make sense, since both are defined using inductive.

The way cases works is to identify each constructor that could have been used to build the evidence and generate a subgoal in which we assume that the evidence was built with that constructor.

Exercise: 3 stars, standard (ev_plus_plus) #

Here's an exercise that just requires applying existing lemmas. No induction or case analysis is needed, but some of the rewriting may be tedious.

theorem ev_plus_plus_simple :  n m p, Even (n + m)  Even (n + p)  Even (m + p) := by
  sorry

Computational vs. Propositional Types #

We have seen that Lean expressions can have types like Nat, which classify computational objects, and Nat → Nat → Nat, which describe functions for computing new objects from old ones, and Prop, which classifies logical propositions, and Nat → Prop, which describes properties of computational objects.

Computational types classify computational objects with the following fundamental property: there is an effective procedure (an algorithm) to check whether any two objects of the type are equal or not.

Propositional types classify logical propositions, which may or may not be provable. Unlike computational objects, propositions are not amenable to computation in general. We cannot simply run an algorithm to check whether a proposition is true or not — this is what proofs are for.

Many computational types are also inhabited by elements with computational content — e.g., the type Nat is inhabited by 0, 1, 2, etc. Some propositions are also inhabited by proofs — e.g., 2 + 2 = 4 is inhabited by rfl : 2 + 2 = 4. But some propositions, like 2 + 2 = 5, are not inhabited by any proofs.

Exercise: 2 stars, standard, optional (bogus_subgoal) #

Normally, we can use cases on the evidence for any inductively defined proposition. This includes properties like ev, beautiful, etc., and also properties like eq and False. The reason this works is that cases can be used to reason about evidence for these properties, not the properties themselves.

However, there is an important exception: for some properties, like True, there is no interesting evidence. In such cases, cases still works, but it doesn't generate any subgoals. Try this to see what happens:

theorem bogus_subgoal : True  False := by
  intro H
  cases H
  -- No subgoals generated!
  sorry

Exercise: 1 star, standard, optional (b_times2) #

Prove that b * 2 is even for any b.

theorem b_times2 :  b : Nat, Even (b * 2) := by
  intro b
  rw [Nat.mul_comm]
  apply ev_double

Additional Exercises #

Exercise: 2 stars, standard, optional (ev_plus_one) #

This exercise explores how hypotheses work in Lean.

theorem ev_plus_one :  n, Even (n + 1)  False := by
  intro n H
  cases H with
  | succ2 n' H' =>
    -- We have ev (n' + 2), but n + 1 = n' + 2 means n = n' + 1
    -- This leads to a contradiction since n + 1 cannot be even
    sorry