Proof terms and meta-theory
For this class we will look a bit more at the theory behind Agda, Lean and Rocq: dependent type theory. We will see how this underlying theory comes into play when proving theorems and defining functions. Knowing this isn't strictly necessary to use Lean or Rocq (for Agda this is a different story), but it will certainly help, for instance to understand error messages a bit more. And sometimes it makes proving easier. It's also relevant if you want to work in the area of proof assistants later.
From Stdlib Require Import List. Import ListNotations.
Dependent functions and type universes
So what makes dependent type theory dependent?
In simple type theory, variants of which are used in OCaml for instance, types
can depend on other types, as exemplified by list A (using Rocq syntax).
Dependent types can additionally depend on terms. This allows types to include
computation or even be computations themselves.
See for instance the if in the type of the function below.
Definition dependent (b : bool) : if b then nat else bool :=
match b with true => 0 | false => true end.We already used such if statements when showcasing Σ types in the previous
lecture so it shouldn't be too surprising.
The thing which allows us to include computation in types is that terms and
types are not actually separated. In particular, types have a type, often
Type, and Type itself has a type!
This brings us to the notion of universe.
Essentially Type and Prop are universes. They contain respectively the
types and propositions (as the names suggest).
Exercise: N-ary functions
Define an n-ary summation function sum, such that e.g.
sum 3 = fun a b c => a + b + c.
Fixpoint nary (n : nat) := match n with | 0 => nat | S n => nat -> nary n end.Definition sum n : nary n := REPLACE_ME.sum 2 = (fun x1 x2 : nat => x1 + x2)Admitted.sum 2 = (fun x1 x2 : nat => x1 + x2)sum 3 = (fun x1 x2 x3 : nat => x1 + x2 + x3)Admitted.sum 3 = (fun x1 x2 x3 : nat => x1 + x2 + x3)
Above you see that Type itself has type Type. It makes sense, after all
Type is indeed a Type. However, assuming Type : Type actually leads to
inconsistencies (proofs of False) so instead, we have a hierarchy of
universes: basically Type₀ : Type₁ : Type₂ …
You can ask Rocq to show you universe levels by using the following command.
Set Printing Universes.Unset Printing Universes.
This is not very easy to read but essentially it says that Typeᵢ lives
in Typeᵢ₊₁.
The fact that the universe levels are not displayed and inferred by the system is sometimes called typical ambiguity.
Even while hidden they still play an important role.
If for instance we alias Type under some name T, we will run into an
issue with the following definition because it will pick a specific universe
level (the index i in Typeᵢ).
Definition T := Type.
Here you get what is called a "universe inconsistency" because you cannot quantify over a universe and stay within it essentially.
Compare however with the following, seemingly equivalent, presentation.
This time it works because the Type on the right has a higher level than
the one on the left so that it is able to contain it.
You don't need to worry too much about universes at this point but know that
they exist to forbid inconsistencies and that sometimes you will get
universe inconsistencies that will require you to look more closely at the
definitions.
We will also have examples about this later, so stay tuned. 😎
Back to Prop now.
Prop is of type Type and actually Prop doesn't have any hidden universe
levels. It however has one interesting property which is that to create a
proposition you can quantify over anything. This includes Prop itself.
This property is called impredicativity and is not present in Agda for
instance.
Proof terms
Let us consider again the even predicate from the previous lesson.
Inductive even : nat -> Prop :=
| evenO : even 0
| evenS : forall n, even n -> even (S (S n)).As we showed you, constructor is actually applying the correct constructor
between evenO and evenS. So when proving even 4 we can actually be
explicit like below:
even 4even 4even 2apply evenO. Qed.even 0
Now let us use Print to see what the proof is to Rocq.
You see two uses of evenS followed by evenO.
We also see that the numbers 2 and 0 are there because they appear as
arguments to evenS but they're not really necessary so we can mark them as
implicit so they don't bother us.
Arguments evenS {n}.Now the proof of even 4 we write directly as follows:
Definition even_four' :=
evenS (evenS evenO).Exercise: Even
Define a proof term of the following type:
Forall, implication
For now you saw what to do with forall and -> interactively, and it was
very similar: you use intro or intros to prove them, and apply to use
them.
When we prove something using tactics we can ask Rocq to show how it built it
with the command Show Proof. Let's see on an example.
forall P Q : Type, P -> (P -> Q) -> Qforall P Q : Type, P -> (P -> Q) -> QP, Q: Type
x: P
f: P -> QQapply x.P, Q: Type
x: P
f: P -> QPQed.
We get the proof term corresponding to the proof: it's a definition whose type is exactly the proposition we wanted to prove. We can use it directly to prove our lemma.
forall P Q : Type, P -> (P -> Q) -> Qapply (fun P Q x f => f x). Qed.forall P Q : Type, P -> (P -> Q) -> Q
We see that function abstraction (or λ-abstraction, the fun … => part)
corresponds to intros, while apply is interpreted as function application
(hence the name).
Coming up with a proof term by hand is hard, and Rocq can also help us write
it using the refine tactic which accepts a partial term with some
underscores (_) and then you can fill them interactively.
forall P Q : Type, P -> (P -> Q) -> Qforall P Q : Type, P -> (P -> Q) -> Q
Basically here we do intros P Q x f.
P, Q: Type
x: P
f: P -> QQ
Now apply f.
P, Q: Type
x: P
f: P -> QP
Now apply x.
refine x. Qed.
The proof in essence hasn't changed. All it comes down to is the typing
rules of the theory. If you know the Curry-Howard correspondence for simple
type theory this shouldn't be surprising.
Let us recap the idea quickly.
Proving A → B is the same as defining a function from A to B.
The introduction rule corresponds to λ-abstraction:
If you don't know λ-calculus, then think of λ x. t as a function taking an
argument x and returning t where t might mention x.
The elimination rule for implication then corresponds to application:
We mentioned before that we have dependent types, that lets us write
things such as forall (x : A), B, where B may refer to x.
This generalises the rules above to quantifiers:
Since x can appear in B, it needs to be replaced by u,
which is what the \(B[x := u]\) represents.
Let us see how it works in practice:
(forall n : nat, even n) -> even 1refine (fun f => f 1).(forall n : nat, even n) -> even 1
f 1 has type even 1, the n was substituted by 1.
Qed.Let us see again proof terms in action and how it compares to tactics.
forall n : nat, even n -> even (4 + n)forall n : nat, even n -> even (4 + n)n: nat
H: even neven (4 + n)n: nat
H: even neven (S (S n))apply H. Qed.n: nat
H: even neven nforall n : nat, even n -> even (4 + n)forall n : nat, even n -> even (4 + n)refine H. Qed.n: nat
H: even neven n
We can just do it in direct style.
Definition even_plus_four_term :
forall (n : nat), even n -> even (4 + n)
:= fun n H => evenS (evenS H).Exercise: Proof terms
Prove the following statements using proof terms.
If it's too hard to do directly, you can use the interactive mode together
with the refine tactic.
Definition ex1 : forall (P Q : Prop), P -> Q -> P := REPLACE_ME. Definition ex2 : forall (P Q R : Prop), P -> (P -> Q) -> (P -> Q -> R) -> R := REPLACE_ME.
Exercise: Less than
We give you this type for "less than" (lt).
Inductive lt (n : nat) : nat -> Prop :=
| lt_B : lt n (S n)
| lt_S m : lt n m -> lt n (S m).Prove the following lemma interactively using intros and apply.
forall n m : nat, lt n m -> lt n (4 + m)Admitted.forall n m : nat, lt n m -> lt n (4 + m)
Now prove it with a proof term.
Definition lt_plus_4' n m : lt n m -> lt n (4 + m) :=
REPLACE_ME.And, or, exists
First, we'll have a look at the definitions of various logical connectives and propositions we've used over the course of the lecture.
Module Show_Definitions.First, False can be seen as the empty type, so we define it as an inductive
type with absolutely no constructor.
We write this as below, with an odd syntax that really just lists all—meaning
0—the constructors.
Inductive False := .
True on the other hand is like the unit type: it has exactly one constructor
which happens to be called I.
Inductive True := I.
~ P is a notation for not P which unfolds to P -> False.
But this you already knew.
Definition not (P : Prop) := P -> False. Notation "~ P" := (not P).
Conjunction A /\ B is a notation for and A B which is defined in a very
similar way to cartesian product A * B, except that A and B, as well
as A /\ B are propositions (they live in the Prop universe).
Inductive and (P Q : Prop) : Prop := | conj : P -> Q -> and P Q.
Disjunction P \/ Q is a notation for or P Q and has two constructors:
one saying P is enough to prove P \/ Q and one saying Q is enough.
Inductive or (P Q : Prop) : Prop := | or_introl : P -> or P Q | or_intror : Q -> or P Q. End Show_Definitions.
You can also use the Print command to see the definitions above.
Note how you can use quotation marks around a notation to print it as well,
which can be quite handy when you don't know what it represents.
To eliminate a proof of False, one can just match on it and provide the
exactly zero cases that are needed.
Definition elim_False : forall (Z : Prop), False -> Z :=
fun Z a =>
match a with end.The rest should be easier to understand.
Definition elim_and : forall (X Y Z : Prop), X /\ Y -> (X -> Y -> Z) -> Z := fun X Y Z a e => match a with | conj x y => e x y end. Definition elim_or : forall (X Y Z : Prop), X \/ Y -> (X -> Z) -> (Y -> Z) -> Z := fun X Y Z a e1 e2 => match a with | or_introl x => e1 x | or_intror y => e2 y end.
Exercise: Connectives as proof terms
Prove the following with a proof term.
Hint: Use Print "\/" if needed.
Definition or_comm P Q : P \/ Q -> Q \/ P :=
REPLACE_ME.Prove the following with a proof term.
Definition ex3 :
forall X (P Q : X -> Prop),
(forall x, P x <-> Q x) ->
(forall x, Q x) ->
forall x, P x
:=
REPLACE_ME.Prove the following with a proof term.
If you have trouble with this one, try to use refine to fill the term
interactively.
Definition Russell X : ~ (X <-> ~ X)
:=
REPLACE_ME.Exercise: Impredicative encodings
Thanks to impredicativity of Prop (the ability to quantify over
propositions within propositions) it is possible to define most connectives
using only forall and -> (no inductive definitions).
For instance, one can define False as follows:
Definition iFalse := forall (P : Prop), P.Show that iFalse is equivalent to False.
False <-> iFalseAdmitted.False <-> iFalse
Define the impredicative encoding of True and show it equivalent to
True.
Definition iTrue : Prop := REPLACE_ME.True <-> iTrueAdmitted.True <-> iTrue
Now do the same thing for conjunction, disjunction and the existential quantifier.
Definition iAnd (P Q : Prop) : Prop := REPLACE_ME.forall P Q : Prop, P /\ Q <-> iAnd P QAdmitted. Definition iOr (P Q : Prop) : Prop := REPLACE_ME.forall P Q : Prop, P /\ Q <-> iAnd P Qforall P Q : Prop, P \/ Q <-> iOr P QAdmitted. Definition iEx (X : Type) (P : X -> Prop) : Prop := REPLACE_ME.forall P Q : Prop, P \/ Q <-> iOr P Qforall (X : Type) (P : X -> Prop), (exists x : X, P x) <-> iEx X PAdmitted.forall (X : Type) (P : X -> Prop), (exists x : X, P x) <-> iEx X P
Using tactics to build definitions
There is no rule that says that tactics are reserved for proofs. Proving and programming are essentially the same thing in Rocq!
add: nat -> nat -> nat
n, m: natnatadd: nat -> nat -> nat
n, m: natnatadd: nat -> nat -> nat
m: natnatadd: nat -> nat -> nat
n, m: natnatapply m.add: nat -> nat -> nat
m: natnatadd: nat -> nat -> nat
n, m: natnatadd: nat -> nat -> nat
n, m: natnatadd: nat -> nat -> nat
n, m: natnatadd: nat -> nat -> nat
n, m: natnatapply n.add: nat -> nat -> nat
n, m: natnatapply m. Defined.add: nat -> nat -> nat
n, m: natnat
We can show that the resulting add we just defined is actually exactly the
same as the one from the standard library.
At least from the point of view of Rocq:
Nat.add = addreflexivity. Qed.Nat.add = add
Modulo some simplification, we get the same thing as what we would have written by hand.
Proofs using terms can be quite short.
forall (X : Type) (p q : X -> Prop), (forall x : X, p x <-> q x) -> (forall x : X, q x) -> forall x : X, p xforall (X : Type) (p q : X -> Prop), (forall x : X, p x <-> q x) -> (forall x : X, q x) -> forall x : X, p xX: Type
p, q: X -> Prop
f: forall x : X, p x <-> q x
g: forall x : X, q x
x: Xp xexact (h (g x)).X: Type
p, q: X -> Prop
f: forall x : X, p x <-> q x
g: forall x : X, q x
x: X
q0: p x -> q x
h: q x -> p xp xQed.
Exercise: Tactics for programs
There is also a fix tactic to introduce fix in the term.
It takes two arguments: fix f n introduces an assumption f in the context,
with the same type as the goal, and n indicates which argument (starting
from 1) is recursive.
So below fix rec 1 introduces rec : nat -> nat -> nat where the first
argument is recursive, a bit like writing rec a b {struct a}.
nat -> nat -> natnat -> nat -> natrec: nat -> nat -> natnat -> nat -> natrec: nat -> nat -> nat
n, m: natnatrec: nat -> nat -> nat
m: natnatrec: nat -> nat -> nat
n, m: natnatapply m.rec: nat -> nat -> nat
m: natnatrec: nat -> nat -> nat
n, m: natnatrec: nat -> nat -> nat
n, m: natnatrec: nat -> nat -> nat
n, m: natnatrec: nat -> nat -> nat
n, m: natnatapply n.rec: nat -> nat -> nat
n, m: natnatapply m. Defined.rec: nat -> nat -> nat
n, m: natnat
First prove the following.
Nat.add = additionAdmitted.Nat.add = addition
Now define an append function on lists (the one we write ++) using the fix
tactic.
A: Typelist A -> list A -> list AAdmitted.A: Typelist A -> list A -> list Aapp = @appendAdmitted.app = @append
Guard condition
If you remember, when we talked about Fixpoint we mentioned {struct n}
and how it was necessary to tell Rocq which argument was structurally
recursive but we never explicitly said why termination is so important in the
first place.
So, let's disable termination checking and find out!
It's actually very easy to derive False from this.
We use the following command to disable the termination checker.
Unset Guard Checking.False is now simply proven via a loop!
Fixpoint f (x : unit) : False := f x.We get back a proof of False by applying the function f.
Falseexact (f tt). Qed.False
Let's not forget to turn termination checking back on. Turning it off is in any case not something we recommend you do, but it's nice for certain exercises.
Set Guard Checking.Exercise: Non-termination (advanced)
There are three crucial potential breaking points of consistency of type theory:
termination of recursive functions, which needs to be ensured;
strict positivity of inductive types, which we see in the lecture right after this exercise;
having type hierarchies rather than a
Type : Typerule.
Define a function of type nat -> nat and use it to deduce False.
Note that this is surprisingly tricky.
Unset Guard Checking. Set Guard Checking.
Strict positivity
The termination checker is not the only important part of the system. Another important criterion, called strict positivity, is imposed on inductive type definitions.
It says that a constructor of an inductive type cannot mention said inductive
type on the left of an arrow (->).
As before, we'll break things by disabling positivity checking.
We once again derive False.
Unset Positivity Checking. Inductive bad := | C : (bad -> False) -> bad.
It already looks fishy because C here has type ~ bad -> bad.
FalseFalse
Remember, the tick notation does pattern matching.
exact (f (C f)). Qed. Set Positivity Checking.f:= fun '(C g) => g (C g): bad -> FalseFalse
Ok, so what happened here?
We first proved f : ~ bad by deducing False from an element of bad.
By case analysis, that element is necessarily of the form C g with g : ~ bad
so we just need to construct an element of bad to conclude with g.
We do so by providing C g.
From f : ~ bad we can once again construct bad and thus deduce a
contradiction.
Don't worry if it makes you scratch your head a bit too much. The exercise below might help by presenting it slightly differently.
Exercise: Non-positive types (advanced)
Define an inductive type bad such that bad <-> ~ bad, use lemma
Russell from above to derive a contradiction.
Unset Positivity Checking. Set Positivity Checking.
Recursors
We've been using induction principles for inductive types quite a bit, but in
Rocq they are not primitive.
They actually unfold to a combination of fix and match.
See below how we define the induction principles for nat and list.
You should have the necessary background to understand these definitions now.
Fixpoint nat_ind (P : nat -> Prop) (z : P 0) (s : forall (n : nat), P n -> P (S n)) (n : nat) : P n := match n with | 0 => z | S n => s n (nat_ind P z s n) end. Fixpoint list_ind {X} (P : list X -> Prop) (n : P nil) (c : forall (x : X) (l : list X), P l -> P (cons x l)) (l : list X) : P l := match l with | nil => n | cons x l => c x l (list_ind P n c l) end.
Exercise: More induction principles
Define induction principles using Fixpoint for all the following types of
trees.
Inductive btree := | bL | bN (b1 b2 : btree). Inductive ltree (L : Type) := | lL (l : L) | lN (l : L) (b1 b2 : ltree L). Inductive atree (L : Type) (B : L -> Type) := | aN (l : L) (bs : B l -> atree L B). Abbreviation altree L := (atree (L * bool) (fun p => if snd p then bool else False)).
Define functions to : forall {L}, ltree L -> altree L
and of : forall {L}, altree L -> ltree L such that
forall L (t : ltree L), of (to t) = t.
Exercise: Nested induction principles
Define an induction principle using Fixpoint for rtree.
You may use the Forall predicate to help you.
Exercise: Fibonacci with recursors (advanced)
Here is the Fibonacci function.
Fixpoint fib n : nat :=
match n with
| 0 | 1 => 1
| S (S n as m) => fib m + fib n
end.Notice the as keyword to give a name to subexpression S n.
This way Rocq knows that S n is a subterm of the one we started with.
Define the Fibonacci function fib using nat_rect directly.
Define it using course-of-values recursion, a form of strong induction shown
below.
Of course you need to prove brec first.
Fixpoint below (P : nat -> Type) (n : nat) : Type := match n with | 0 => unit | S n => P n * below P n end.Definition brec {P : nat -> Type} (F : forall n, below P n -> P n) : forall n, P n := REPLACE_ME.
Reduction
Up till now we've been using simpl a lot without really telling you what it
does. Essentially it triggers what we call reduction rules. Those correspond
to the evaluation of programs.
The first rule that applies is called β-reduction.
What it does is turn (fun x => t) u into t[x := u], which, remember, is a
notation for t where every (free) occurrence of x is replaced by u.
Or in more mathy, λ-calculus notation:
We can see it in action by using the Eval command. It takes as argument a
reduction strategy (such as simpl).
Now simpl does much more than β so if we want to only perform β we do:
cbn stands for call-by-name and is a strategy that can be parametrised by
flags such as beta to limit its action.
Here we have limited it to β-reduction. Indeed, one can notice that neither
+ nor * have been reduced.
This way we get control over computation.
We have not really seen them, but it's also possible to introduce let
bindings in definitions as a way to name intermediary results.
Expression let x := u in t can be reduced (using ζ-reduction) by
substituting u for x in t.
Unfolding definitions
There are several ways to unfold definitions.
The first one is with the unfold strategy.
unfold foo will unfold the constant foo.
Another one is to perform δ-reduction which simply replaces names by the
corresponding definitions. A list of names can be provided but is optional.
If no name is provided then all possible δ-reductions are performed.
To use delta we switch to cbv which stands for call-by-value.
Definition foo := 2 + 9. Definition bar := let x := 10 in x * x. Definition baz := foo + bar.
The output from the previous function might be surprising to you. Let's look at a simpler example to understand what is happening.
What happened is that Rocq here also unfolded the definition of + (which is
Nat.add) which is defined through a combination of Fixpoint (or fix) and
pattern matching (match).
Strategies like cbn or simpl attempt to be nicer on the user and actually
refold definitions such as + in the end because it's much easier to read.
More reduction rules
fix and match have their own set of reduction rules of the same name.
The combination of the two is called ι-reduction (ι like [i]nductive).
We will see how it goes in interactive mode where we can also use unfold,
cbn or cbv as tactics (exactly like simpl).
2 + 1 = 32 + 1 = 3(fix add (n m : nat) {struct n} : nat := match n with | 0 => m | S p => S (add p m) end) 2 1 = 3
This performs one unfolding of the fix:
match 2 with | 0 => 1 | S p => S ((fix add (n m : nat) {struct n} : nat := match n with | 0 => m | S p0 => S (add p0 m) end) p 1) end = 3
It's still there in the recursive branch!
Now we reduce to the right branch:
S ((fix add (n m : nat) {struct n} : nat := match n with | 0 => m | S p => S (add p m) end) 1 1) = 3
One more step:
S match 1 with | 0 => 1 | S p => S ((fix add (n m : nat) {struct n} : nat := match n with | 0 => m | S p0 => S (add p0 m) end) p 1) end = 3S (S ((fix add (n m : nat) {struct n} : nat := match n with | 0 => m | S p => S (add p m) end) 0 1)) = 3
Or we use ι-reduction which does it all at once:
Abort.3 = 3
To summarise, the available reduction rules are
β for function application;
ζ for letin;
δ for constants;
match for matches;
fix for fixpoints;
ι combining fix and match.
Controlling unfolding
Sometimes we want to simplify an expression but only focusing on some parts.
Say in the expression (S n + m) - m from last week where we only wanted
to reduce the + part. We showed simpl add but there are other ways to do
it.
For instance, you can give to cbn a list of definitions it can unfold.
Notice how we can use the notation directly!
forall n m : nat, S n + m - m = nforall n m : nat, S n + m - m = nn, m: natS n + m - m = nAbort.n, m: natS (n + m) - m = n
Sometimes we'd rather provide a list of exceptions not to be unfolded. For this we use the minus symbol before the list.
forall n m : nat, S n + m - m = nforall n m : nat, S n + m - m = nn, m: natS n + m - m = nAbort.n, m: natS (n + m) - m = n
Below is a demo of examples of proof terms. In particular we show you cases where the proof term is much shorter than using tactics.
Section Demo. Variables X Y Z: Prop.
Note implicit argument overloading of or_introl and or_intror.
ex falso quodlibet:
X, Y, Z: Prop~ (X <-> ~ X)X, Y, Z: Prop~ (X <-> ~ X)X, Y, Z: Prop
f: X -> ~ X
g: ~ X -> XFalseX, Y, Z: Prop
f: X -> ~ X
g: ~ X -> XXX, Y, Z: Prop
f: X -> ~ X
g: ~ X -> X
x: XFalseX, Y, Z: Prop
f: X -> ~ X
g: ~ X -> XXX, Y, Z: Prop
f: X -> ~ X
g: ~ X -> X~ Xexact (f x x).X, Y, Z: Prop
f: X -> ~ X
g: ~ X -> X
x: XFalseexact (f x x).X, Y, Z: Prop
f: X -> ~ X
g: ~ X -> X
x: XFalseQed.X, Y, Z: Prop~ (X <-> ~ X)X, Y, Z: Prop~ (X <-> ~ X)X, Y, Z: Prop
a: X <-> ~ X
f: X -> ~ X
g: ~ X -> XFalseX, Y, Z: Prop
a: X <-> ~ X
f: X -> ~ X
g: ~ X -> XXexact (f x x).X, Y, Z: Prop
a: X <-> ~ X
f: X -> ~ X
g: ~ X -> X
x: XFalseQed. End Demo.
Exercise: Type : Type (advanced)
We are going to prove a theorem allowing to prove False from
Type : Type.
Fill in the missing parts below.
Module Hierarchy. Definition embeds X Y := exists (E : X -> Y) (D : Y -> X), forall x, D (E x) = x.X: Typeembeds X XAdmitted. Definition Tyi := Type. Inductive tree (A : Tyi) (D : A -> Tyi) : Tyi := | T (a : A) (f : D a -> tree A D). Arguments T { _ _}. Definition sub {A : Tyi} {D : A -> Tyi} (s t : @tree A D) : Prop := match t with | T a f => exists x, f x = s end.X: Typeembeds X XA: Tyi
D: A -> Tyiforall s : tree A D, ~ sub s sAdmitted.A: Tyi
D: A -> Tyiforall s : tree A D, ~ sub s sA: Type
D: A -> Tyi
E: Tyi -> A~ embeds (tree A D) (D (E (tree A D)))Admitted.A: Type
D: A -> Tyi
E: Tyi -> A~ embeds (tree A D) (D (E (tree A D)))forall X : Tyi, ~ embeds Tyi XAdmitted.forall X : Tyi, ~ embeds Tyi X
We now enable the Type : Type rule.
Unset Universe Checking.FalseFalseapply embeds_refl. Qed. End Hierarchy.embeds Tyi Tyi