16 Predicates and relations in Rocq
From Stdlib Require Import Utf8.
From Stdlib Require Import Lia.In this file, we formalise some of the results on predicates and relations that we have proven in the lectures.
16.1 Predicates
Recall that a predicate on a set X is a function P : X → Prop.
16.1.1 Examples
For instance, let us define inductively what it means for a natural number to be even.
Inductive IsEven : nat → Prop :=
dbl (n : nat) : IsEven (n + n).The definition says that dbl n is evidence for the fact that n + n is even. More formally, this inductive definition formalises the fact that a natural number m is even if we can write it as n + n for some natural number n. For instance, we can prove the following basic results.
Example zero_is_even : IsEven 0 :=
dbl 0.
Example six_is_even : IsEven 6 :=
dbl 3.Note that IsEven 3 type-checks. Of course, we want to prove that this proposition does not hold, which as usual means that its negation, namely the proposition IsEven 3 → False, does hold.
Check IsEven 3.Proving IsEven 3 → False seems not so easy with our definition: one has to show that ‘3 is divisible by 2’ leads to an absurdity. One idea to do that is to argue that if 3 = n + n, then 3 = 2 * n, so 3 mod 2 = (2 * n) mod 2. This should lead to 1 = 0, which we know is an absurdity. This works if we have an explicitly defined mod function (so 3 mod 2 computes to 1) and a proof (by induction on n) that, for all n : nat, (2 * n) mod 2 = 0. It is possible to follow this approach, but it takes time if we redefine everything.
Alternatively, we can trust Rocq’s Lia library that we imported at the top of the file. The name Lia stands for linear integer arithmetic and this library can be used to prove basic inequalities or solve (linear) equations. Note how we use the Unicode character ≤ to denote the inequality <= between natural numbers.
Example ineq1 (n m : nat) (H : n ≤ m) : 2 * n + 3 ≤ 2 * m + 3.
Proof.
lia.
Qed.
Example solve_eq1 (n : nat) (eq1 : n + 8 = 4 * n + 2) : n = 2.
Proof.
lia.
Qed.If you want to get a sense of what lia does behind the scenes, you can always print the resulting proof term.
Print ineq1.
Print solve_eq1.Note that the lia tactic is quite powerful: it can for instance use assumptions from the context to close the goal.
Next, to prove that 3 is not even, we use lia to assert that n + n = 3 is impossible. To do so, we will use the inversion tactic, which introduces a new equation in our context. Namely, inverting the proof H of IsEven 3 means saying that it suffices to look at the case when this proof is the form dbl n for some natural number n such that n + n = 3. The natural number n and the equation n + n = 3 then get added to the context.
This is similar to what destruct H eqn: H0 would do, but keeping track of the equation 3 = n + n. In contrast, destruct H eqn: H0 keeps track of the equation H = dbl n, calling it H0, and replaces IsEven 3 with IsEven (n + n) in the context, which is the main purpose of destruct.
Example three_is_not_even : IsEven 3 → False.
Proof.
intro H.
inversion H as [n Hn].
assert (f : n + n ≠ 3) by lia.
(* apply f; assumption. *)
exact (f Hn).
Qed.As mentioned earlier, you can always print the resulting proof term.
Print three_is_not_even.A lot remains hidden, though, even with the following.
Set Printing All.
Print three_is_not_even.
Unset Printing All.16.1.2 Logically equivalent predicates
Let P₁, P₂ : X → Prop be predicates on X. Such predicates are called equivalent if, for all x : X, the propositions P₁ x and P₂ x are equivalent, meaning that P₁ x ↔︎ P₂ x holds.
To illustrate this, let us propose an alternative definition of what it means for a natural number to be even.
Inductive IsEven' : nat → Prop :=
| zero_even : IsEven' 0
| plus_two : ∀ {n : nat}, IsEven' n → IsEven' (n + 2).
Example zero_is_even' : IsEven' 0 :=
zero_even.
Example six_is_even' : IsEven' 6 :=
plus_two (plus_two (plus_two zero_even)).When proving IsEven' 3 → False, we note that if we want to give specific names to the data introduced by the inversion tactic, we need to consider two cases, because IsEven' has two constructors. The data in question should correspond to the arguments of the constructors. Since the first constructor, namely zero_is_even, has no arguments, the first set of data is empty, hence the line inversion H as [ | n Hn1 Hn2] below. This line could also just be inversion H, letting Rocq take care of naming the variables.
Note that the inversion tactic is also capable of saying that the first case does not occur. This is due to the fact that a term of type IsEven' 3 cannot be introduced by using the constructor zero_even. Indeed, zero_even only produces terms of type IsEven' 0 and 0 is different from 3. Rocq takes care of that for us and does not generate a subgoal corresponding to this case.
Example three_is_not_even' : IsEven' 3 → False.
Proof.
intro H.
inversion H as [ | n Hn1 Hn2].
assert (n = 1) as Hn3 by lia.
rewrite Hn3 in Hn1.
clear H Hn2 Hn3 n.
(* Note: The previous three lines can be condensed to two lines using the `replace` tactic. *)
(* replace n with 1 in Hn1 by lia.
clear H Hn2. *)
inversion Hn1.
assert (n + 1 = 0) by lia.
clear H H0.
Search (S _ ≠ 0).
apply (PeanoNat.Nat.neq_0_succ n).
Search (_ + 1 = S _).
rewrite <- PeanoNat.Nat.add_1_r.
(* symmetry; assumption. *)
easy.
Qed.We now prove that the propositions IsEven n and IsEven' n are logically equivalent.
Theorem IsEven_iff_IsEven' (n : nat) : IsEven n ↔ IsEven' n.
Proof.
split.
- intro H.
destruct H.
induction n as [ | n IHn].
+ simpl.
exact zero_even.
+ replace (S n + S n) with ((n + n) + 2) by lia.
apply plus_two.
exact IHn.
- intro H.
induction H as [ | n Hn].
+ exact (dbl 0).
+ destruct IHHn.
replace (n + n + 2) with ((n + 1) + (n + 1)) by lia.
exact (dbl _).
Qed.It is quite instructive to see what happens when we are actually careful in the definition of IsEven' and take into account the computational behaviour of the addition of natural numbers (which defines n + m by primitive recursion on n, not on m). Namely, we replace n + 2 by 2 + n in the definition.
Inductive IsEven'' : nat → Prop :=
| even_zero : IsEven'' 0
| two_plus : ∀ {n : nat}, IsEven'' n → IsEven'' (2 + n).See now how the proof that 3 is not even gets significantly simplified. This makes this third and last definition of being an even natural number the best of the three, in my view. The next topic, on the sum of even numbers, will show similar behaviour.
Example three_is_not_even'' : IsEven'' 3 → False.
Proof.
intro H.
inversion H.
inversion H0.
Qed.In contrast, the proof of the equivalence between IsEven'' n and IsEven n remains essentially unaltered.
Theorem IsEven_iff_IsEven'' (n : nat) : IsEven n ↔ IsEven'' n.
Proof.
split.
- intro H.
destruct H.
induction n as [ | n Hn].
+ exact even_zero.
+ replace (S n + S n) with (2 + (n + n)) by lia.
apply two_plus.
exact Hn.
- intro H.
induction H as [ | n Hn].
+ exact (dbl 0).
+ destruct IHHn.
replace (2 + (n + n)) with ((1 + n) + (1 + n)) by lia.
exact (dbl _).
Qed.16.1.3 Sum of even numbers
Let us compare our three predicates for even numbers on a simple example. Namely, we want to prove that the sum of two even numbers is even.
With the predicate IsEven, Theorem add_even does not require an induction on the proof of that n is even, but it does require some rewriting (for which we use lia).
Theorem add_even (n m : nat) : IsEven n → IsEven m → IsEven (n + m).
Proof.
intros n_even m_even.
destruct n_even as [p], m_even as [q].
replace (p + p + (q + q)) with (p + q + (p + q)) by lia.
(* apply dbl. *)
constructor.
Qed.With the predicate IsEven', we have to use induction and lia.
Theorem add_even' (n m : nat) : IsEven' n → IsEven' m → IsEven' (n + m).
Proof.
intros n_even' m_even'.
induction n_even' as [ | p Hp].
- assumption.
- replace (p + 2 + m) with (p + m + 2) by lia.
constructor. assumption.
Qed.With the predicate IsEven'', we only need use induction. However, for some reason, the infoview exposes the definition of Nat.add, which is not very readable. Fortunately, we can get rid of it using the fold tactic, which is optional here.
Theorem add_even'' (n m : nat) : IsEven'' n → IsEven'' m → IsEven'' (n + m).
Proof.
intros n_even'' m_even''.
induction n_even'' as [ | p Hp].
+ assumption.
+ apply two_plus.
(* fold (p + m). *)
assumption.
Qed.16.2 Relations
Recall that a relation on a set X is a function R : X → X → Prop. For instance, let us define inductively what it means for a natural number n to be lower or equal than a natural number m.
Inductive leq : nat → nat → Prop :=
| zero_leq (n : nat) : leq 0 n
| mono (m n : nat) : leq m n → leq (S m) (S n).Adding the following line makes the constructors of leq accessible to automation tactics such as easy and auto.
Hint Constructors leq.
Infix "≤" := leq (at level 70, no associativity).
Example zero_leq_three : 0 ≤ 3 :=
zero_leq 3.
Example two_leq_five : 2 ≤ 5 :=
mono 1 4 (mono 0 3 zero_leq_three).Let us now prove that the relation leq is reflexive.
Theorem leq_reflexive : ∀ n : nat, n ≤ n.
Proof.
intro n.
induction n.
+ easy.
+ apply mono.
assumption.
Qed.As a next step, let us prove that the relation leq is also transitive.
Theorem leq_transitive : ∀ p q r : nat, p ≤ q → q ≤ r → p ≤ r.
Proof.
intros.
revert r H0.
induction H.
+ easy.
+ intros.
inversion H0.
apply (IHleq n0) in H2.
apply mono.
assumption.
Qed.To prove that the relation leq is anti-symmetric, the following lemma will be helpful.
Lemma leq_zero_is_zero : ∀ n : nat, n ≤ 0 → 0 = n.
Proof.
intros n Hn.
inversion_clear Hn.
reflexivity.
Qed.
Theorem leq_antisymmetric : ∀ p q : nat, p ≤ q → q ≤ p → p = q.
Proof.
intros.
induction H.
+ apply leq_zero_is_zero.
assumption.
+ apply f_equal.
apply IHleq.
inversion_clear H0.
assumption.
Qed.It is little noticeable because we are so used to it, but the antisymmetry property of leq is not just a property of leq, it is also a property of = on nat. Indeed, the type p ≤ q → q ≤ p → p = q specified above involves both relations. For that reason, it is perhaps useful to try and characterise equality of natural numbers in an elementary way.
Inductive nat_eq : nat → nat → Prop :=
| zero_eq_zero : nat_eq 0 0
| succ_eq (p q: nat) : nat_eq p q → nat_eq (S p) (S q).
Infix "≡" := nat_eq (at level 70, no associativity).Let us start by proving that the relation nat_eq is reflexive. The proof goes by induction on the batural number q in the context.
Lemma nat_eq_reflexiv (q : nat) : q ≡ q.
Proof.
induction q.
+ exact zero_eq_zero.
+ apply succ_eq.
assumption.
Qed.Then we can show that nat_eq is logically equivalent to the equality relation we have been using all along.
Theorem nat_eq_charac (p q : nat) : p ≡ q ↔ p = q.
Proof.
split.
+ intro.
induction H.
- reflexivity.
- apply f_equal; assumption.
+ intro.
rewrite H.
apply nat_eq_reflexiv.
Qed.16.3 Preorders, orders, and equivalence relations
We have proven several properties about the relations leq : nat → nat → Prop and nat_eq : nat → nat → Prop. Using another construct of Rocq, we can be slightly more formal about this, and introduce the type of equivalence relations on a set, as well as the type of orderings on a set, and show that nat_eq and leq define terms in those types.
The abstract construct we need is that of a record type in Rocq, which gives us a way to formalise tuples of various kinds. From the point of view of programming language theory, a record can be thought of as an object (the latter being defined conceptually as a list of key-value pairs).
16.3.1 Preorders and orders
Let us start with the example of pre-orders. A pre-order on a set X is a triple consisting of:
- a relation
RonX. - a proof that
Ris symmetric. - a proof that
Ris transitive.
We can then declare the type of preorders on a set X as follows.
Record PreOrder (X : Set) := PreOrderMk {
ord_rel : X → X → Prop;
ord_refl : ∀ x : X, ord_rel x x;
ord_trans : ∀ x y z : X, ord_rel x y → ord_rel y z → ord_rel x z
}.The resulting term PreOrder is a function that takes a set X and returns a type. One might expect it to return a set, but the fact that Prop is involved in the definition and that Prop is not a set in Rocq, accounds for terms like PreOrder nat being of sort Type and not Set. This is a minor theoretical point for us right now, that does not affect how we reason with the collection of preorders.
Check PreOrder.
Check PreOrder nat.The function PreOrderMk, whose name was chosen explicitly by us, acts like a constuctor for terms of type PreOrder X. It will be convenient to make the parameter X implicit.
Arguments PreOrderMk {X}.
Check @PreOrderMk nat.With all the work we have done, we can construct an explicit order relation on nat.
Definition pre_order_on_nat : PreOrder nat :=
PreOrderMk leq leq_reflexive leq_transitive.When a record type is declared, a few functions are created automatically, that correspond to the fields (keys) in the record. Here our PreOrder X type has three fields, namely ord_rel, ord_refl and ord_trans (these names were chosen by us), so we get three so-called projection functions.
It will be convenient to make the parameter X implicit. Indeed, the first argument of the field projections can usually be inferred in pratice, which is why it is a good idea to make it implicit.
Arguments ord_rel {X}.
Arguments ord_refl {X}.
Arguments ord_trans {X}.
Check @ord_rel.
Check @ord_refl.
Check @ord_trans.With these functions, we can access the values corresponding to each field in our record pre_order_on_nat. With a bit more work, we could set up a dot notation for it, and write expressions such as pre_order_on_nat.ord_rel to access our relation leq : nat → nat → Prop.
Check ord_rel pre_order_on_nat.
Goal (ord_rel pre_order_on_nat = leq).
Proof.
reflexivity.
Qed.
Check ord_refl pre_order_on_nat.
Check ord_trans pre_order_on_nat.
Definition pre_order_on_nat' : PreOrder nat.
Proof.
apply (PreOrderMk leq).
- exact leq_reflexive.
- exact leq_transitive.
Qed.Order relations should extend preorders, by adding the property that the relation is anti-symmetric. In Rocq, this can be achieved using the following syntax, in which we make PreOrder the type of a field of the Order structure.
Record Order (X : Set) := OrderMk {
toPreOrder :> PreOrder X;
ord_antisymm : ∀ x y : X, ord_rel toPreOrder x y → ord_rel toPreOrder y x → x = y
}.
Arguments OrderMk {X}.
Definition order_on_nat : Order nat :=
OrderMk pre_order_on_nat leq_antisymmetric.For every X : Set, we get a projection @toPreOrder X : Order X → Preorder X.
Check toPreOrder.
Arguments toPreOrder {X}.The symbol :> in the definition of Order registers the projection toPreorder as a coercion from Order X to PreOrder X, so Rocq will automatically insert it when a term of type PreOrder X is expected but one of type Order X is given.
This makes expression like ord_rel nat order_on_nat type-check, even though the function ord_rel expects a preorder relation on nat and order_on_nat is in fact an order relation.
Check ord_refl order_on_nat.
Check ord_refl (toPreOrder order_on_nat).We can also remove the symbol :> from the declaration Order X and implement a coercion to PreOrder X by hand, as seen in a previous lecture. Note that doing this does not affect the next lines of the declaration: ord_rel X toPreOrder still type-checks.
16.3.2 Equivalence relations
Similarly to what we did for preorders, we define equivalence relations on a set X as quadruples consisting of:
- a relation
RonX. - a proof that
Ris reflexive. - a proof that
Ris symmetric. - a proof that
Ris transitive.
Note that Rocq lets us introduce the notation ~ for a relation such as equiv_rel whilst still in the middle of the declaration of the type EquivRel. This is convenient because it makes the next lines much more readable (but you need to ‘book’ the notation in advance).
We can also make all the parameters whose types can be inferred from later arguments implicit by default. This will affect the constructor EquivRelMk as well as the field projections. Finally, note that, in the declaration EquivRel, we specify the type of EquivRel X explicitly.
Set Implicit Arguments.
Reserved Notation "x ~ y" (at level 70, no associativity).
Record EquivRel (X : Set) : Type := EquivRelMk {
equiv_rel : X → X → Prop
where "x ~ y" := (equiv_rel x y);
equiv_refl : ∀ x : X, x ~ x;
equiv_symm : ∀ x y : X, x ~ y → y ~ x;
equiv_trans : ∀ x y z : X, x ~ y → y ~ z → x ~ z
}.
About EquivRelMk.
Fail Check equiv_rel nat.
About equiv_rel.As an exercise, you can show that nat_eq is an equivalence relation on nat, hence defines a term of type EquivRel nat.
16.4 Records and inductive declarations
There is a way to pass from record declarations to inductive declarations with just one constructor and vice-versa. We will not get into details here and just illustrate this with two examples.
16.4.1 Product types
As we know, product types can be declared inductively.
Inductive MyProd (X Y : Set) :=
pair : X → Y → MyProd X Y.They can also be declared as a record.
Record MyProd' (X Y : Set) := MyProd'Mk {
fst : X;
snd : Y
}.We see on this example that the arguments of the constructor pair for the inductive type MyProd X Y are in one-to-one correspondence with the fields of the record MyProd' X Y. One advantage of the record approach is that the projections to the fields are generated automatically.
16.4.2 The Unit type
Another example is the Unit type, whose only constructor takes no arguments (atomic constructor). As a consequence, the corresponding record has no fields!
Inductive MyUnit :=
pt : MyUnit.
Record MyUnit' := MyUnit'Mk {}.The set MyUnit' has at least one element.
Check MyUnit'Mk.In fact, it also has at most one element, in the following sense.
Example at_most_one_element : ∀ s t : MyUnit', s = t.
Proof.
intros.
destruct s, t.
reflexivity.
Qed.