LARA

Coq tutorial

Welcome to this basic Coq tutorial! Before we start, be sure to have Coq installed and a IDE configured for Coq ready to go. Please check the installation page for instructions on how to install Coq and an IDE for Coq.

A presentation of the tutorial was given live over Zoom. A recording of that Zoom meeting is available here.

Programming in Coq

We will first have a look at Coq as a programming language.

Data types

First, we will see how to define datatypes. In Coq, we can define algebraic datatypes using the Inductive construct. For instance, to define booleans, one simply needs to write:

 Inductive bool : Type :=
  | true
  | false.
 

The name of the type defined here is bool, and the type as two constructors: true and false.

Note the point at the end of the definition. Each command in Coq is ended by a single point.

Important note: If you are following along in your IDE (which we highly suggest you do), be sure to interpret commands as we go along. Your IDE should have a button / command to interpret forward by one command, or to interpret to the current cursor.

To check the type of an expression, you may find useful to use the Check command. Simply type and interpret the following command to see the type of true:

 Check true.
 

The result (which is true : bool) should appear in a panel of your IDE.

We can also define inductive data types in Coq. For instance, here is a definition of generic lists in Coq:

 Inductive list (A : Type): Type :=
  | nil : list A
  | cons : A -> list A -> list A.
 

Notice that list takes a type parameter A. We write list A for a list of elements of type A. If you check the type of nil and of cons, you will see that they have as a first argument the type A.

 Check nil.
Check cons.
 

Therefore, if we were, for instance, to build a list containing the bools true and false, we would have to explicitly pass the type parameter bools to all constructors.

 Check (cons bool true (cons bool false (nil bool))).
 

Coq however support implicit parameters. To declare the type parameter A of the two constructors as implicit, we can use the following commands:

 Arguments nil {A}.
Arguments cons {A}.
 

Once this is done, we can build lists without explicitly passing the type argument.

 Check (cons true (cons false nil)).
 

If you wish to be able to explicitly pass arguments to implicit parameters, simply add an @ before the name of the definition:

 Check (@nil bool).
 

Natural numbers and pairs

There are a few datatypes that are defined by default in Coq. Natural numbers and pairs are two such datatypes.

Natural numbers are of type nat. The type is also defined inductively. The constructor is O for 0 and the second S for the successor. Coq also has built-in syntax for natural numbers, so that you can write 0 for O, 1 for S O, 2 for S (S O) and so on.

The type of pairs of values of types T1 and T2 is written as T1 * T2. Pairs have a single constructor. We write the pair of x and y as (x, y).

 Check (0, 1).
 

Option

One final noteworthy construct of Coq is the option type constructor, which is used to represent optional values.

You can check the definition of option and of its two constructors Some and None by issuing the command:

 Print option.
 

Functions

We have seen how to define inductive types and their constructors. In this section, we will see how to define functions on those datatypes.

As a first example, consider the following definition for boolean negation:

 Definition negb (b: bool): bool :=
  match b with
  | true => false
  | false => true
  end.
 

The name of the function is negb and it takes a single parameter b of type bool. The return type of the function is bool.

The body of the function is a single match expression. The first case indicates that, when b is true, then the result is false. The second case specifies that, when b is false, then the result is true.

When performing pattern matching, Coq makes sure that the patterns cover all cases. The wildcard pattern _ is used to match against all remaining cases. Such a pattern is useful for instance in the following definition of boolean conjunction:

 Definition andb (b1 b2: bool): bool :=
  match (b1, b2) with
  | (true, true) => true
  | _ => false
  end.
 

Recursive functions

The Definition command that we have used so far is quite restrictive in the sense that it does not allow for recursion. To build a recursive function, one must use a different command than Definition, namely Fixpoint. Here is how one could define the length of a list as a recursive function:

 Fixpoint length {A : Type} (xs: list A): nat :=
  match xs with
  | nil => 0
  | cons x xs' => S (length xs')
  end.
 

Note that the type parameter A is enclosed in curly brackets, which indicates that the parameter is implicit.

Similarly, here is how we can define concatenation of lists:

 Fixpoint app {A : Type} (xs ys: list A): list A :=
  match xs with
  | nil => ys
  | cons x xs' => cons x (app xs' ys)
  end.
 

And here is how one could write list reversal using an helper recursive function with an accumulator:

 Fixpoint rev_acc {A: Type} (xs acc: list A): list A :=
  match xs with
  | nil => acc
  | cons x xs' => rev_acc xs' (cons x acc) 
  end.

Definition rev {A: Type} (xs: list A): list A := rev_acc xs nil.
 

Let and if expressions

Finally, before we move on to theorems and proofs, it is worth mentioning that Coq also supports let expressions and if expressions.

 Compute (let x := 3 in x + x).
Compute (if andb true false then 1 else 0).
 

Also, note the command Compute to compute the result of expressions!

It may seem strange that the built-in if construct of Coq works on our own definition of boolean. If fact if expressions are simply syntactic sugar for pattern matching when the condition type has exactly two constructors. The then branch corresponds to the first constructor, while the else branch to the second. This means that you can also have natural numbers as conditions of if expressions:

 Compute (if 0 then true else false).
Compute (if 42 then true else false).
 

Propositions and proofs

We are now at the point where we can start with the most interesting part of Coq: proving theorems. A theorem is a proposition that has been proven.

Propositions

In Coq, propositions are of type Prop. Just like types, propositions can be defined inductively using the Inductive command. There are a few already defined propositions:

You can check the definitions of proposition True and False by issuing the following commands:

 Print True.
Print False.
 

Note that the propositions True and False are different from the booleans true and false. true and false are constructors of the type bool, while True is a proposition with a single constructor I, and False is a proposition with no constructors at all. It is therefore impossible to build a proof of False.

Equality between two values of the same type also yields a proposition:

 Check (1 = 1).
Check (1 = 2).
 

To express that two values are not equal, we use the infix binary operator <>.

Implication is denoted by ->. For instance, here is the proposition that False implies True:

 Check (False -> True).
 

Negation is Coq is denoted not, or by the prefix operator ~. Negation is defined as implies False, which you can see by printing the definition of not.

 Print not.
 

Disjunction and conjunctions are respectively or and and. Often, we will use the infix binary operators \/ and /\ to refer to or and and. We encourage you to print the definitions of or and and.

Equivalence (“if and only if”) between propositions is named iff in Coq, and can written using the infix binary operator . The proposition iff A B is defined to be (A → B) /\ (B → A).

Finally, coq supports quantifiers in propositions. The keyword for universal quantification forall and for existential quantification is exists. The following propositions showcase how to use such quantifiers:

 Check (forall (n : nat), n = 0).
Check (exists (n : nat), forall (m : nat), n = 0 \/ n = m).
 

Proofs

Now that we know how to express propositions, it is finally time to prove some theorems! To state a theorem, use the command Theorem.

 Theorem app_nil_l : forall (T : Type) (xs : list T), app nil xs = xs.
 

The theorem is given a name, in this case app_nil_l. The proposition to be proven appears after the colon. In this example, the proposition states that, for any type T and list xs of type list T, if we concatenate nil to the left of xs then we get xs.

The command Lemma also exists and is equivalent to Theorem. Lemma however indicates to the reader the relative importance of the proven proposition.

Once the theorem is stated, we must actually prove it! To do so, we use the Proof command.

 Proof.
 

If you interpret the file up until that point, you should see that your IDE displays in some panel the proposition to be proven. To introduce all universally quantified parameters, as well as all hypotheses, we can use the tactic intros.

   intros T xs.
 

After interpreting that command, you should see in your IDE that the goal as now changed and that two arguments T and xs have been added to the context. It is also possible to omit the names and simply call intros, in which case Coq will choose names for you.

The next step in this proof is to apply some simplification. Since the definition of app does pattern matching on the left argument, it is possible to reduce the left side of the equality. To do so, simply invoke the tactic simpl.

   simpl.
 

Now, the goal should take the form of an equality between two equal terms. To solve this goal, simply use the reflexivity tactic.

   reflexivity.
 

After interpreting that tactic, the IDE should inform you that the proof is complete. Finally, to conclude the proof, simply add the command Qed.

 Qed.
 

In the rest of this tutorial, we will look at different tactics and see example proofs in which they are used.

''unfold''

The unfold tactic is used to replace a name by its definition. Below is an example of a use of that tactic:

 Lemma rev_rev_acc : forall A (xs : list A),
  rev xs = rev_acc xs nil.
Proof.
  intros.
  unfold rev.
  reflexivity.
Qed.
 

Many tactics will unfold definitions freely. In fact, reflexivity is such a tactic, and the above proof can simply be reduced to a single call to reflexivity.

''rewrite''

The rewrite tactic is used to rewrite a subterm into the goal given an equality between the subterm and some other term. Here is an example of its use:

 Lemma negb_false_negb_negb_true : forall b,
  negb b = false -> negb (negb b) = true.
Proof.
  intros b H.
  rewrite -> H.
  simpl.
  reflexivity.
Qed.
 

After intros the context will contain the boolean b and the hypothesis H that negb b = false. Thanks to H, we can rewrite the subterm negb b into false. We can then proceed by calling simpl and reflexivity.

''destruct''

Some proofs will require you to do some case analysis. To do so, you may use the tactic destruct, as shown in the following example theorem:

 Theorem double_negb : forall (b : bool), negb (negb b) = b.
Proof.
  intros b.
  destruct b eqn:Eqb.
  + simpl. reflexivity.
  + simpl. reflexivity.
Qed.
 

The call to destruct b performs a case analysis on the boolean b. Since b is of type bool, it must be of the form of a constructor of bool (either true or false). After the call to destruct, the goal will be split into multiple subgoals, in this case two. In each subgoal, the value b be will replaced by true, respectively false, which allows us to eventually prove each subgoal. The proposition Eqb, with will contain either b = true or b = false depending on the subgoal, will also be added to the context. Note that the name Eqb is arbitrary and can be changed.

We use the bullet + to indicate that we handle the next subgoal. Subgoals can be nested. Indeed, it is entirely possible to perform case analysis within a subgoal. At each level of nesting a different bullet symbol should be used amongst +, - or *.

Note that it is possible to destruct compound expressions as well. In this case, it is often important to keep a trace of what the destructed expression was by not removing the eqn:E part.

When all subgoals start with the same prefix of tactics, it is possible to factor them out by using the ; operator, as exemplified by:

 Theorem double_negb' : forall (b : bool), negb (negb b) = b.
Proof.
  intros b.
  destruct b eqn:Eqb; simpl; reflexivity.
Qed.
 

When preceded by a ; a tactic applies to all current subgoal.

''induction''

Sometimes, case analysis is not enough and inductive reasoning must be employed. For this purpose, Coq has the induction tactic.

 Theorem app_nil_r : forall A (xs : list A),
  app xs nil = xs.
Proof.
  intros.
  induction xs.
  + simpl. reflexivity.
  + simpl. rewrite -> IHxs.
    reflexivity.
Qed.
 

In that proof, the first case corresponds to the base case nil, while the second case corresponds to the inductive case cons. In the subgoal for the cons case, we are also provided with an induction hypothesis IHxs which states that app xs nil = xs. Using the rewrite tactic, we can rewrite occurences of app xs nil in the goal into xs.

Using the ; operator, this proof could also be written as:

 Theorem app_nil_r' : forall A (xs : list A),
  app xs nil = xs.
Proof.
  intros.
  induction xs; simpl.
  + reflexivity.
  + rewrite -> IHxs.
    reflexivity.
Qed.
 

''inversion''

The tactic inversion is used to do case analysis on the various ways a hypothesis may be true.

This tactic is also useful when an hypothesis contains an equality between two expressions of the same structure, such as in:

 Theorem cons_head_eq : forall T (x1 x2 : T) xs,
  cons x1 xs = cons x2 xs ->
  x1 = x2.
Proof.
  intros T x1 x2 xs H.
  inversion H.
  reflexivity.
Qed.
 

In that case, the call to inversion H generates the hypothesis that x1 = x2, and also rewrites it in the goal. The call to reflexivity completes the proof.

Also, inversion is very useful to handle contradictions in the hypotheses. When used on a contradictory hypothesis, inversion will immediately solve the current goal.

 Theorem negb_neq : forall b, negb b <> b.
Proof.
  intros b A.
  destruct b.
  + inversion A.
  + inversion A.
Qed.
 

The about proof makes use of the fact that negb b <> b is definitional equal to negb b = b → False. Therefore the hypothesis A initial contains the proposition negb b = b. In the two subgoals after destruct, the hypothesis turns into false = true respectively true = false, which inversion knows to be impossible.

''apply''

The apply tactic is used to apply a hypothesis or an existing lemma to the current goal. The conclusion of the hypothesis must match the current goal.

 Lemma modus_ponens : forall (P Q : Prop),
  (P -> Q) -> P -> Q.
Proof.
  intros P Q H p.
  apply H.
  apply p.
Qed.
 

It is also possible to use apply in a hypothesis in the context. In this case, the hypothesis in the context acts as an argument to the hypothesis applied.

 Lemma modus_ponens' : forall (P Q : Prop),
  (P -> Q) -> P -> Q.
Proof.
  intros P Q H p.
  apply H in p.
  apply p.
Qed.
 

''split''

We the current goal is a conjunction, we can split each side into its own subgoal using the split tactic.

 Lemma app_eq_nil: forall A (xs ys : list A),
  app xs ys = nil ->
  xs = nil /\ ys = nil.
Proof.
  intros A xs.
  destruct xs.
  - intros. simpl in H.
    split.
    + reflexivity.
    + apply H.
  - intros. inversion H.
Qed.
 

''left'' and ''right''

When the goal is a disjunction, we can choose which side to prove by either using left or using right.

 Lemma length_non_zero_or_nil : forall T (xs : list T),
  xs = nil \/ length xs <> 0.
Proof.
  intros T xs.
  destruct xs.
  - left.
    reflexivity.
  - right.
    simpl.
    intros A.
    inversion A.
Qed.
 

''exists''

When the goal is of the form exists x, …, you can apply the tactic exists to fix the existential quantified x to a term.

 Theorem larger_lists: forall T (x : T) (xs : list T), 
  exists (xs' : list T),
  length xs' = S (length xs).
Proof.
  intros T x xs.
  exists (cons x xs).
  simpl.
  reflexivity.
Qed.
 

Note that when you have an existentially quantified proposition as hypothesis, you may destruct it to get an actual witness. The above theorem could be rewritten as the following to exemplify this:

 Theorem larger_lists': forall T (xs : list T), 
  (exists x : T, True) ->
  exists (xs' : list T),
  length xs' = S (length xs).
Proof.
  intros T xs E.
  destruct E.
  exists (cons x xs).
  simpl.
  reflexivity.
Qed.
 

Searching

As a final tip, Coq also offers searching facilities to help you find useful lemmas in the standard library. The Search commands accepts a pattern and will look for theorems and definitions where the pattern matches (some part of) the signature.

 Search plus.
Search (S _ = S _ -> _ = _).