coq_lecture

Require Import Coq.Init.Prelude.

Introduction

Coq is an interactive proof assistant. It helps us write formal proofs and verify their correctness. There are many reasons why we would want computer assistance. Mathematicians are human and we are prone to make mistakes. We would like to be able to verify correctness of proofs we write. Sometimes the proofs are too complex for anyone besides the author to check by hand. It can be also very useful to have a machine keep track of the details that go into a proof, and verify that we don't miss anything. Lastly, we can sometimes leave easy computations to a machine. Sometimes proving something involves tedious work of checking many easy cases. This is exactly the kind of task computers are good at.
You can download and install Coq from https://coq.inria.fr/, but an easier way for you to start using it would be to run a javascript IDE from your browser at https://jscoq.github.io/scratchpad.html.
This document is generate from the Coq file available at http://www.math.jhu.edu/~vzakharevich/teaching/spring2020/coq_lecture.v . The best way for you to follow is to copy it into the Coq IDE in your browser and run the commands as we go along.

IDE

You should first familiarize yourself with the navigation and the screens of the IDE. There are navigation buttons at the top right corner of the screen that let you execute one command at a time. It will be more convenient to use the keyboard for it though (Alt-Up and Alt-Down). The top right screen window, which should say goals is where the structure of proofs will be displayed. We will get to it in due time. The bottom right screen is the message screen. It will display errors and outputs of commands. One such command is Check:

Check(4).

The message window should display
4
     : nat
which, as we will soon see, indicates that 4 is a natural number. Notice that Check(4). ends with a period. Just as in most natural languages, statements in Coq end with a period.

Types

Every object you will encounter in Coq will be of a particular type. In general, the relation that A is of type B is indicated by A:B. To see what type an object A is, we can run the Check(A). command. For example, we learned that 4: nat, or in other words 4 has type nat. We can then ask ourselves, what type is nat?

Check(nat).

You should see the output nat:Set indicating that nat is of type Set. This is probably what you expected: natural numbers form a set. Perhaps a better way of saying this relation is that 4 is a natural number and nat is a set. If we try going further we learn

Check(Set).

that Set is of type Type, or that Set is a Type. We will not need a deeper understanding of types, but the thing to take away is that Type is some collection of terms and Set is one of those terms. This is similar to how Set is a collection of terms (sets), and nat is one of those terms (which itself is a collection of elements e.g. 4). Besides Set, there is another fundament type, namely the type of propositions.

Check(Prop).

Let's define a propositional variable.

Section section1.

Variable(P:Prop).

Check(P).

Variable (p:P).

Check(p).

End section1.

First we learned that P is a proposition, but then we were able to define a term of type P. What can that mean? In Coq, propositions are not abstractly true or false. They are things that might have proofs. In that vein, propositions should be thought of as collections of their proofs, of which for example there could be multiple or none. Therefore the statement p:P can be interpreted as p is a proof of P, or in other words, we know that P is true because of p.
There are fundamental propositions True and False, corresponding to a tautology and a contradiction

Check(True).
Check(False).

Since True is a tautology, there is an intrinsic proof of it denoted I.
Check(I).

First Theorem

Without further ado, let's see what a theorem and a proof look like.
Theorem theorem_1 (P: Prop) : P -> P /\ P.
Proof.
  intro proof_of_P.
  split.
  -
  exact(proof_of_P).
  -
  exact(proof_of_P).
Qed.
Now, let me explain what is happening here line by line.

Theorem theorem_1 (P: Prop) : P -> P /\ P.

This is the declaration of the theorem. The general format is Theorem [name] [context]: [Statement]. So in our case, we're calling the theorem theorem_1, the context of the theorem is that P is a proposition and the statement of the theorem is that in this context P -> P /\ P is true. If you run Check(P -> P /\ P). you will see that P -> P /\ P is a proposition. It is the proposition constructed from P using the logical operators we discussed in class, namely P implies P and P. The only way in Coq to assert that a proposition is true is to provide a proof of it. Therefore theorem_1 should give rise to a term of type P -> P /\ P. Notice how the statement of the theorem is consistent with this view: ignoring the context part for a moment, the theorem statement reads theorem_1 : P -> P /\ P, i.e. theorem_1 is a proof of P -> P /\ P.

Proof.
This is the declaration of the beginning of the proof. Look at the "goals" screen. You should see
1 subgoal
P : Prop
______________________________________(1/1)
P -> P /\ P
Everything above the horizontal line is your context, or the things we know or have. The proposition below the line is the goal of the theorem. The task is to manipulate what we know and the goal until we are able to deduce the goal. The ways we are allowed to manipulate the context and the goals are based on the rules of inference that we have learned as well as the theorems that we have already proved.
Since we are trying to prove an implication P -> P /\ P, it suffices to assume the validity of P and deduce P /\ P. Assuming the validity of P in Coq mean assuming we have a proof of P. The command intro proof_of_P. tells Coq to add a proof of P, which we call proof_of_P to the context and update the goal to P /\ P. Note that after running this command our context contains proof_of_P : P. This corresponds to applying the introduction rule of implication to the goal.

  intro proof_of_P.
We are now tasked with proving P/\P. It being a conjunction, it suffices to prove P and P separately. The command split. will break up our goal into two separate goals which we will need to prove. This corresponds to the introduction rule of conjunction.

  split.

Now that there are two goals to prove, we need to focus Coq on the first goal (this is not strictly necessary), which is accomplished by -.
  -
  
At this point we notice that our goal is P and we have a proof of P, namely proof_of_P in our context. We tell Coq that this completes the proof of the goal with the command
  exact(proof_of_P).
At this point Coq notifies us that we still have another goal to prove. To focus on it, we use - again.
  -
  
Again, our goal is P and we have a proof of it in our context.
  exact(proof_of_P).
At this point Coq tells us that there is nothing else left to prove, so we are done.
Qed.
At this point, we can rest assured that we have proved theorem_1 correctly. We can also use our theorem in future proofs. Let's see what kind of object our theorem will allow us to construct. Let's first introduce some propositional variable my_prop.
Section section2.

Variable (my_prop:Prop).

Then theorem_1 my_prop is a term of my_prop -> my_prop /\ my_prop which itself is a proposition. In other words, theorem_1 my_prop is a proof of my_prop -> my_prop /\ my_prop.
Check(theorem_1 my_prop).

Check(my_prop -> my_prop /\ my_prop).

End section2.

Let's see how we can use theorem_1 in a proof.

Theorem theorem_2 (P:Prop): P->P/\P/\P.
Proof.
  intro p.
  split.
  -exact(p).
  -
  
Up to this point, we have only used what we have already seen. Now the state of the proof is
  1 subgoal
P : Prop
p : P
______________________________________(1/1)
P /\ P
In other words we have a proof of P and we want to deduce P /\ P. This is exactly what theorem_1 allows us to do. We have that theorem_1 P:P->P/\P, which is an implication. We can apply this implication to p:P in order to deduce P/\P. In other words theorem_1 P p: P/\ P. The command pose, gives a new name to this term. So here, we're going to call theorem_1 P p by proof_of_p_and_p, because that is what it is. (We didn't have to give it a separate name and could have passed it to exact directly).
   pose (proof_of_p_and_p:= theorem_1 P p).
  exact(proof_of_p_and_p).
Qed.

Rules of inference

Hopefull at this point you got the hang of how proofs work in Coq and would be able to prove theorems involving propositions once I tell you how to apply the rules of inference that we've learned in class. Unfortunately, the commands corresponding to the rules are not very uniform and it takes some time getting used to their use. Nonetheless, here is the table with the commands corresponding to the rules. You should come back to it now and then as we work through more proofs below.
Introduction Elimination
/\ split goal
pose(p_and_q:= conj p q) destruct p_and_q
(specialize p_and_q as [p,q])
context
\/ apply(or_introl)
apply(or_intror)
goal
pose(p_or_q:= or_introl p : P\/Q)
pose(p_or_q:= or_intror q : P\/Q)
case p_or_q
(destruct p_or_q)
context
-> intro p
intros
refine(p_implies_q _) goal
assert(p_implies_q: P -> Q) pose(q:= p_implies_q q) context
~A use unfold to rewrite ~A as A->False
The types of the terms in the table are as follows:
P Q : Prop
p:P, q:Q
p_and_q: P/\Q
p_or_q: P\/Q
p_implies_q: P->Q

Let's work through some examples together.

Theorem theorem_3 (P Q:Prop) : P/\Q -> Q/\P.
Proof.

Statements of theorems often have the form of an implication. In that case, the proof would usually start with assuming the antecedent with the consequent becoming the new goal. This corresponds to the introduction rule of implication applied to the goal, which is achieved by the command intro

  intro p_and_q.

We need to prove the conjunction P/\Q, so we should split the goal into two goals: P and Q. This corresponds to applying the introduction rule of conjunction to the goal which is accomplished by split.

  split.

We now have two goals. Let's focus on the first one
  -
  
At this point we know P/\Q and we would like to deduce Q. We can apply the elimination rule to P/\Q to deduce Q
  destruct p_and_q.
Our goal is now in the context, so we use exact to complete the proof.
  exact(H0).
Coq tells us that there is still a subgoal to prove, so we focus on that subgoal. Its proof is analogous to the proof of the first goal.
  -
  destruct(p_and_q).
  exact(H).
Qed.

We have seen in class that P/\Q->R is logically equivalent to P->Q->R. The latter form is more common and happens to be more fundamental in Coq, so it is good to get used to interpreting P->Q->R as P and Q implies R. Let's see the proof of this statement.

Theorem theorem_4 (P Q R:Prop): (P->Q->R) <-> (P/\Q -> R).
Proof.
Recall that biconditional operator P<->Q is shorthand for P->Q/\ Q->P, so we can split it into two subgoals
  split.

intros introduces all assumptions of implications in the goal and chooses generic names for them. It's the introduction rule of -> applied to the goal.
  - intros.

destruct acts as the elimination rule of conjunction
   destruct H0.

Here, we introduce a proof of Q->R by taking the proof H of P->(Q->R) and combining it with the proof H0 of P. It corresponds to the elimination rule of ->. The command pose is used in general when we need to add an assumption to the context by say applying a function. The term H: P -> Q -> R can be though of as a function that to a term of type P assigns a term of type Q->R. The expression H H0 can be though of as applying H to H0.

    pose (q_implies_r := H H0).

Next line applies the elimination rule of -> to the goal. We already have a proof q_implies_r of Q->R. Therefore to prove R, it suffices to prove Q.
     refine (q_implies_r _).

      exact(H1).

For the following subgoal, try to explicitly identify the introduction and elimination rules used
  - intros.
  refine (H _).
  split.
  exact(H0).
  exact(H1).
Qed.

Here are few more examples.

Theorem theorem_5 (P Q :Prop): P\/Q -> Q\/P.
Proof.
  intros.
The elimination rule of disjunction says that to deduce R from P\/Q we need to seperately, deduce R from P and deduce R from Q. Recall that this leads to the strategy "proof by cases". Luckily, the term for the corresponding operator in Coq is case.
   case H.
   - intros.
   apply(or_intror).
If you run Check(or_intror)., the output is [or_intror : forall A B : Prop, B -> A \/ B] In other words, or_intror is the universal proof that B implies A\/B. Applying it to the goal Q\/P, changes the goal to P. This corresponds to the introduction rule of disjunction applied to the goal.
  assumption.
If our goal is already in the context, we can let Coq find it to complete the proof. The corresponding command is assumptions., i.e. we can use it instead of exact.
  - intros.
  pose (proof_of_q_or_p := or_introl H0:Q\/ P).
In this subgoal, we went the way of applying the disjunction introduction rule to create a new term in the context.
  assumption.
Qed.

Can you follow the steps of the following proof?
Theorem theorem_6 (P Q R :Prop): (P /\ (Q \/ R) -> (P /\ Q) \/ (P /\ R)).
Proof.
  intros.
  destruct H.
  case H0.
  - intros.
      apply(or_introl).
      split.
      assumption.
      assumption.
  - intros.
      apply(or_intror).
      split.
      assumption.
      assumption.
Qed.

Coq also has an automatic solver which can prove any theorem we have seen so far.

Theorem theorem_7 (P Q R : Prop) : ((P /\ Q) \/ (P /\ R) -> P /\ (Q \/ R)).
Proof.
  intuition.
Qed.

Quantifiers

In order to talk about quantifiers, we first need to define predicates. We already mentioned that terms of type P->Q where P,Q are propositions can be though of as functions which to a proof of P assign a proof of Q. It shouldn't be surprising then that the collection of function from A to B has type A->B for any pair of types A,B which can be thought of as collections (Set,Prop, or Type). A predicate on some collection A is then a term of type A->Prop. A predicate P: A-> Prop gives rise to propositions [ forall x:A, P x] and [ exists x:A, P x ]
Just like for logical operators, there are introduction and elimination rules for quantifiers which we summarize in the following table.
Introduction Elimination
forall intros goal
assert(proof_of_forall: forall x:X, P x) specialize (anything) context
exists refine(ex_intro _ witness _) goal
assert(proof_of_exists: exists x:X, P x) destruct(proof_of_exists) context
Here is an example of a proof using quantifiers.

Theorem theorem_8 (X Y: Set) (P: X->Y->Prop): (exists x:X, forall y:Y, P x y)-> (forall y:Y, exists x:X, P x y).
  Proof.
  intros.
  destruct H.
  specialize (H y).
  refine(ex_intro _ x _).
  exact(H).
Qed.

Natural numbers

So far, we have only proved things involving propositions and predicates. I would like to now give you an idea of what more complicated constructions and proofs look like in Coq. Let's first talk about natural numbers. The definition of the set of natural numbers follows Peano's axioms.

Inductive nat : Set :=
  | O : nat
  | S : nat -> nat.

This defines a set nat with two ways of constructing elements belonging to it: there is an element O and there is a succesor function S:nat->nat which can be used to construct elements. For example, S(S(O)) corresponds to number 2.
Check(S(S(O))).
Coq also defines objects necessary to operate on nat inductively which you should see in the message window. If you run Check(nat_ind)., you'll see that it's precicely the statement of the theorem allowing us to prove statements about natural numbers by induction. We will see such proof in a moment, but first, let's define addition recursively.

Fixpoint add (n m: nat) : nat:=
  match n with
  |O => m
  | S n0=> S( add n0 m)
  end.

Check(add).

Note that add has type nat -> nat -> nat. Recall that for P,Q,R propositions, a term of type P->Q->R can be thought of as a function that to a proof of P and a proof of Q assigns a proof of R. In the same way, a term of type nat -> nat -> nat, assigns a natural number to a pair of natural numbers, i.e. it's a function from nat x nat to nat, as the addition function should be.
As our first proof, let's prove that adding 0 to a natural number leaves it unchanged. This time, we will state the theorem using the universal quantifier.

Theorem theorem_9 : forall m :nat, add O m = m.
Proof.
  intros.
  simpl.
The tactic simpl evaluates the function. In this case, it looks at the definition of add and picks out the first constructor. In this case, it is applied to the goal.
  reflexivity.
The tactic reflexivity verifies the validity of the proposition x=x for any x
Qed.

Theorem theorem_10: forall n:nat, add n O=n.
  intros.
  simpl.
Notice that simpl. had no effect since the argument of add is not manifestly in the form in which it appears in the constructor of add. In order to proceed with the proof, we need to argue by induction on n.
  elim n.
For n a natural number, elim n. starts the proof by induction on n. It creates two subgoals, one corresponding to the base case and one corresponding to the inductive step.
  -
  
Base case
   simpl.
   reflexivity.

  -
  
Inductive step
  intros.
  simpl.
Notice that simpl. evaluated the function add (S n0) O since the argument had the form of one of the constructors of add
  rewrite H.
The rewrite tactic uses an equality a=b in the context in order to rewrite the term a in the goal as b. If the equality goes in the wrong direction, use rewrite <- instead.
  reflexivity.
Qed.

Here is a much more challenging theorem. I recommend you try proving it on your own before looking through the proof.

Theorem theorem_11: forall n:nat, forall m: nat, add n m= add m n.
Proof.

  intros.
  elim m.
  elim n.
  - simpl. reflexivity.
  - intros. simpl. rewrite H. simpl. reflexivity.
  - intros. simpl. rewrite <- H.
  elim n. simpl. reflexivity.
  intros. simpl. rewrite H0. reflexivity.

Qed.

Function injectivity

For our last example, we will prove that composition of injective functions is an injective function. We first define what it means for a function to be injective.

Definition inject (dom cod:Set) (f: dom -> cod) := forall x:dom, forall y:dom, (f x= f y) -> (x = y).

This defines a predicate on triples (dom, cod, f) where dom and cod are sets and f is a function from dom to cod. The predicate is exactly the one expressing that the function f is an injection, i e. that for all x,y in dom, if f(x)=f(y), then x=y.

Definition comp ( A B C : Set) (f: A -> B) (g: B -> C) := fun a => g (f a).

This defines the composition of functions. A pair of composable functions is given by three sets A,B,C, and functions f:A -> B and g:B-> C. The notation fun a => g (f a) specifies the function that sends a to g(f(a)). The type of a is left implicit here, which Coq determines from the context.
Here is the proof that the composition of injective functions is injective. It uses a new tactic unfold which we haven't used yet. What unfold does is it expands a definition, either in the goal (here unfold inject.) or in a term in the context ( unfold inject in H). Try to follow each step of this proof.

Theorem comp_of_inj_is_inj : forall A B C f g, inject A B f -> inject B C g -> inject A C(comp A B C f g).
Proof.

  intros.
  unfold inject.
  unfold inject in H.
  unfold inject in H0.
  intros.
  unfold comp in H1.
  specialize (H0 (f x ) (f y)).
  specialize (H x y).
  exact(H (H0 H1)).
Qed.

This page has been generated by coqdoc