MPRI PRFA — Proof Assistants
🏠 Go back to the course homepage.
Proof terms and meta-theory — Part 2
Sequel to ⬅️ part 1.
From Stdlib Require Import List. Import ListNotations.
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!
Note that instead of Qed we need to put Defined to tell Rocq we care about the content of the definition.
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 m.add: nat -> nat -> nat
n, m: natnat
Modulo some simplication, 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 x
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.
Actually, if we deactivate the termination checking, we can derive a proof of False very easily.
Unset Guard Checking.
False is now simply proven via a loop!
We get back a proof of False by applying the function f.
Falseexact (f tt).FalseSet Guard Checking.
The termination checker is not the only important part of the system. One such criterion is called strict positivity and is imposed on inductive types definitions.
It says that a constructor to an inductive type cannot mention said inductive type on the left of an arrow (->). See the following example when deactivating positivity checking from which we can derive False.
Unset Positivity Checking.
It already looks fishy because C here has type ~ bad -> bad.
FalseFalse
Remember, the tick notation does pattern matching.
exact (f (C f)).f:= = fun '(C g) => g (C g): bad -> FalseFalseSet Positivity Checking.
Recursors
Induction principles in Rocq are not primitive, they actually unfold to a combination of Fixpoint and match.
See below how we define them for nat and list.
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]
.
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.
Note. δ is like [d]efinition.
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 add) which is defined through a combination of Fixpoint (or fix) and pattern matching (match).
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
Controlling unfolding
Somethimes 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
There are more strategies like lazy, hnf, or vm_compute…
📖 More detail can be found in the reference manual.
Here 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.
Note implicit argument overloading of or_introl and or_intror.
exfalso 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: XFalseX, 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: XFalseEnd Demo.
ADVANCED
Mutual inductive types
It is also possible to define several inductive types at the same time. You just combine them with the with keyword.
This way we can define the type of trees mutually with that of forests (which are basically lists of trees).
You can then define mutual definitions over such types by using Fixpoint and the with keyword.
Nested inductive types
Finally, you can define more inductive types by using what is called nesting. In the type below, you define a tree as something that contains a list of trees.