Inductive types
This week, we will talk about inductive types. If you are familiar with functional programming, inductive types are very similar to the algebraic data types of OCaml and Haskell. With a more mathematical or logical mindset, they can be thought of as sets constructed inductively: closed under a finite collection of generators. They are quite expressive, and can for instance be used to express certain logical predicates as we shall see.
Declaring inductive types
Inductive types can be defined using the Inductive keyword.
Below, we give the definition of natural numbers nat. They are literally the
same as the ones from the standard library which we have been using until now.
The type nat is made of two constructors: O and S.
We have O : nat and S n : nat if n : nat.
Rocq uses BNF (Backus-Naur-form) notation for inductive types:
Inductive nat : Type :=
| O : nat
| S : nat -> nat.
If you use Check (or hover the constants) you will see they have exactly the
types we gave them in the description above:
nat is a type (and thus of type Type),
O is a nat,
S has type nat -> nat, meaning it's a function taking a nat, say
n, and returning another nat, called S n.
Elements of the type nat now consist exactly of O, S O, S (S O), and so
on:
The Inductive command allows syntactic flexibility.
We could also have written the following:
Inductive nat := | O : nat | S (n : nat) : nat.
or without the first pipe (|) and the return type annotations:
Inductive nat := O | S (n : nat).
In practice you get to pick the variant that suits you the most as they are anyway equivalent: they merely differ in how you write them, but produce the same definition.
You can actually check that they produce the same thing yourself by using the
Print command on the inductive type nat:
As the word inductive type hints, one can do proofs by induction about them. Mathematically, an inductive type is the smallest type closed under the type's constructors. You are already familiar with induction on natural numbers from the previous chapter. However, make sure that you are also familiar with the exact formulation of the induction principle:
As we have seen earlier,
one can define recursive functions operating on inductive arguments with the
Fixpoint keyword.
We also already had a look at addition, but this time we'll look at its Rocq
syntax.
It operates by recursion on its first argument, n, looking at which
constructor (O or S) is at its head.
Operationally, it's looking at all the S on the left, and pushing them
outside the addition, and once we reach O, the second argument m is
returned.
Fixpoint add (n m : nat) {struct n} :=
match n with
| O => m
| S n => S (add n m)
end.Rocq ensures that recursive functions are terminating, for reasons we will discuss later.
To ensure termination, recursion has to be performed on a structurally smaller
argument. In the case of double from last week, there was only one argument
so it was obvious which one had to decrease. This time, there is an ambiguity:
it could be either n or m. The {struct n} annotation above tells
Rocq that n is the one that is structurally decreasing.
Of course, it won't blindly trust us, there is a complex termination checker
(sometimes called guard checker, and you will hear "guard condition" too) which
will make sure this is indeed the case.
For instance, if you were to suggest that m was structurally decreasing,
Rocq would rightfully complain (which we indicate with the keyword Fail):
The important bit of the error message is the one saying:
Recursive call to add' has principal argument equal to "m" instead of a subterm of "m".
In other words it's complaining about the recursive call being performed on
m again.
Evaluating functions
It is possible to evaluate a function, such as the add function above by
using the Eval command.
Its first argument is an evaluation strategy (and we shall see more about
that later), for now we use simpl, the very same we've been usinig inside
proofs to simplify the goal.
The term to evaluate is after the in:
Note how the following evaluates to fun m => m (the identity function):
However, the following expression cannot be simplified using computation.
The reason, is that computation really follows from the definition.
We told Rocq that the main argument was the first (with {struct n}) and so it
will not start computing until this one has a constructor in its head.
A variable doesn't start with a constructor, so the expression is stuck.
This doesn't mean that add n O is different from O.
There are indeed equal and we can prove it by induction, rather than by
computation.
n: natadd n O = nn: natadd n O = nadd O O = On: nat
IHn: add n O = nadd (S n) O = S nreflexivity.add O O = On: nat
IHn: add n O = nadd (S n) O = S nn: nat
IHn: add n O = nS (add n O) = S nreflexivity. Qed.n: nat
IHn: add n O = nS n = S n
Let us now introduce an infix notations for add.
Infix "+" := add.See how Rocq prints the lemma statement using + now!
Now, even if we still use add, Rocq will print the goal using +.
n, m: natn + S m = S (n + m)n, m: natn + S m = S (n + m)m: natO + S m = S (O + m)n, m: nat
IHn: n + S m = S (n + m)S n + S m = S (S n + m)reflexivity.m: natO + S m = S (O + m)n, m: nat
IHn: n + S m = S (n + m)S n + S m = S (S n + m)n, m: nat
IHn: n + S m = S (n + m)S (n + S m) = S (S (n + m))reflexivity. Qed.n, m: nat
IHn: n + S m = S (n + m)S (S (n + m)) = S (S (n + m))
We now define (truncating) subtraction on natural numbers. It is sometimes called monus.
Fixpoint sub (n m : nat) : nat :=
match n, m with
| O, _ => O
| S n', O => n
| S n', S m' => sub n' m'
end.
Instead of using Infix to give a notation to sub we can also use the
Notation command. It asks for more information and is sometimes tricky to
use but it is very powerful.
We will see more examples over the course of the lecture so don't worry too much
about it.
Notation "n - m" := (sub n m).An alternative to Eval simpl in e is Compute e.
See what it does to the following two expressions.
We can now prove a lemma involving addition and subtraction that we just defined.
n, m: natn + m - n = mn, m: natn + m - n = mm: natO + m - O = mn, m: nat
IHn: n + m - n = mS n + m - S n = mm: natO + m - O = mm: natm - O = mall: reflexivity.O - O = Om: natS m - O = S massumption. Qed.n, m: nat
IHn: n + m - n = mS n + m - S n = m
Above, we used a new construct all:, it tells Rocq to apply the tactic that
follows (and only the one tactic) to all subgoals at this point.
This is relative to the focused goals, so the successor case of the induction is
not a target of reflexivity here. It will fail if the tactic fails in even
just one of the goals.
Predicates, propositions and injectivity of constructors
In Rocq, everything is computational, including propositions.
We can define the following Boolean predicate testing whether a natural number
is O or not.
Definition is_zerob (n : nat) : bool :=
match n with
| O => true
| S n => false
end.But we can also produce directly a proposition asserting asserting whether
the natural number is O or not.
Pay attention to the use of Prop instead of bool and of capital True
and False.
Definition is_zero (n : nat) : Prop :=
match n with
| O => True
| S n => False
end.Prop is a bit like Type in that it classifies types, but there is a
different intention behind Prop, its types are to be understood as
propositions.
Things like True, False and n = m are propositions, and we shall see more
of them.
Of course, a proposition stating that n is O could equivalently be
written as n = O as we have seen already.
Similarly, we can express what it means to be different from O by writing
n <> O, which is Rocq's way of writing n ≠ 0 (in fact, if you use
Unicode with Rocq you can
also write n ≠ 0 directly).
This notation is nothing more than the negation of n = O, in other words it
unfolds to (n = P) -> False.
We can use this to prove the fact that, in general, constructors are distinct.
For nat this means that O and S n can always be distinguished.
n: natS n <> On: natS n <> O
This can be proved manually
n: nat
E: S n = OFalse
We use the change tactic to change the goal to something equivalent with
respect to computation.
n: nat
E: S n = Ois_zero (S n)
We can simplify again to get the old goal.
This is in fact the reason why change succeeded in the first place.
n: nat
E: S n = OFalsen: nat
E: S n = Ois_zero (S n)
Now that our goal is is_zero (S n) we want to use our hypothesis
E : S n = O to get is_zero O instead. We can do this using the
rewrite tactic which will replace occurrence of S n in the goal by the
right-hand side of the equation, namely O.
n: nat
E: S n = Ois_zero O
Computation is now handy again, because is_zero O is much easier to prove
than False. We simplify and then conclude.
split. Qed.n: nat
E: S n = OTrue
There is also a tactic to automatically discharge such goals: discriminate.
Behind the scenes, it's essentially doing the above, by constructing a predicate
that discriminates S n and O.
n: natS n <> Odiscriminate. Qed.n: natS n <> O
Say now we want to prove that S is injective. We can achieve this by
using an auxiliary function pred such that pred (S n) = n
(in other words, it is a left inverse to S).
By congruence (f_equal) we can thus go from S n = S m to
pred (S n) = pred (S m) which is then the same as n = m.
You can see here that it doesn't matter what value we choose for pred O.
We're just going to pick O. In a sense it's only a partial predecessor.
Definition pred n :=
match n with
| O => O
| S n => n
end.We can now prove that S is injective.
We will do it in several different ways to illustrate the possibilities.
The first one uses change and is close in structure to the proof of the S_O
lemma above.
n, m: natS n = S m -> n = mn, m: natS n = S m -> n = mn, m: nat
E: S n = S mn = mn, m: nat
E: S n = S mpred (S n) = mreflexivity. Qed.n, m: nat
E: S n = S mpred (S m) = m
We could in fact do it without pred, in a way that's even closer to S_O.
n, m: natS n = S m -> n = mn, m: natS n = S m -> n = mn, m: nat
E: S n = S mn = mn, m: nat
E: S n = S mmatch S n with | O => True | S k => k = m endreflexivity. Qed.n, m: nat
E: S n = S mm = m
You may notice that after using rewrite, Rocq got rid of the pattern matching
block (match) even we didn't ask it to do so with simpl.
This will happen in some cases and we hope it's not too magical for your taste.
We can also use the f_equal lemma (not the tactic!).
n, m: natS n = S m -> n = mn, m: natS n = S m -> n = mn, m: natS n = S m -> n = mn, m: nat
E: S n = S mn = massumption. Qed.n, m: nat
E: pred (S n) = pred (S m)n = m
Finally, we have tactics that do this automatically such as inversion.
n, m: natS n = S m -> n = mn, m: natS n = S m -> n = mn, m: nat
E: S n = S mn = mreflexivity. Qed.n, m: nat
E: S n = S m
H0: n = mm = m
Generalising induction hypotheses
Let us now try to prove things about functions operating on inductive types. We're going to start by defining a function for deciding equality between natural numbers.
Fixpoint eq_nat x y : bool :=
match x, y with
| O, O => true
| O, S _ => false
| S _, O => false
| S x', S y' => eq_nat x' y'
end.We're going to prove that it indeed decides equality of natural numbers.
Below, we again make use of the notation P <-> Q for logical equivalence.
See above.
n, m: nateq_nat n m = true <-> n = mn, m: nateq_nat n m = true <-> n = mm: nateq_nat O m = true <-> O = mn', m: nat
IH: eq_nat n' m = true <-> n' = meq_nat (S n') m = true <-> S n' = mm: natmatch m with | O => true | S _ => false end = true <-> O = mn', m: nat
IH: eq_nat n' m = true <-> n' = mmatch m with | O => false | S y' => eq_nat n' y' end = true <-> S n' = m
We made use of all: simpl. here to apply the simpl tactic to all subgoals,
before even focusing them. all is called a goal selector by the way.
m: natmatch m with | O => true | S _ => false end = true <-> O = mtrue = true <-> O = Om: natfalse = true <-> O = S mtrue = true <-> O = Om: natfalse = true <-> O = S mtrue = true <-> O = Oall: reflexivity.true = true -> O = OO = O -> true = true
Notice reflexivity is able to conclude A -> 0 = 0 for any A.
m: natfalse = true <-> O = S mm: natfalse = true -> O = S mm: natO = S m -> false = trueall: discriminate E.m: nat
E: false = trueO = S mm: nat
E: O = S mfalse = true
Here we provided E to discriminate to tell it which assumption was
inconsistent (equating two distinct constructors).
n', m: nat
IH: eq_nat n' m = true <-> n' = mmatch m with | O => false | S y' => eq_nat n' y' end = true <-> S n' = mn': nat
IH: eq_nat n' O = true <-> n' = Ofalse = true <-> S n' = On', m: nat
IH: eq_nat n' (S m) = true <-> n' = S meq_nat n' m = true <-> S n' = S mn': nat
IH: eq_nat n' O = true <-> n' = Ofalse = true <-> S n' = Oall: discriminate.n': nat
IH: eq_nat n' O = true <-> n' = Ofalse = true -> S n' = On': nat
IH: eq_nat n' O = true <-> n' = OS n' = O -> false = truen', m: nat
IH: eq_nat n' (S m) = true <-> n' = S meq_nat n' m = true <-> S n' = S m
We are now stuck. The IH talks about S m, but we need it for m.
Note that this always happens when a recursive function changes other arguments
apart from its structural argument.
We give up for now:
Abort.The proper way to deal with this is by generalising the induction hypothesis.
We have to do this before we start perfoming induction.
The reason we have to do this is because Rocq is trying to infer the property
we prove by induction on its own: we only say we want to perform induction on
n and it then tries to infer the property from the goal.
Fortunately, the induction tactic also supports generalisation with the
keyword in, as follows.
n, m: nateq_nat n m = true <-> n = mn, m: nateq_nat n m = true <-> n = mm: nateq_nat O m = true <-> O = mn', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = meq_nat (S n') m = true <-> S n' = m
The syntax is bit heavy: m |- * represents which information should Rocq
take into account when generating the induction hypothesis. The turnstile |-
is supposed to represent the whole proof goal, with the hypotheses on the left,
and the proposition to prove on the right. We use * to indicate that we want
to include the goal (which you always will want to do) and m on the left to
indicate we generalise over it, and nothing else.
m: nateq_nat O m = true <-> O = meq_nat O O = true <-> O = Om: nateq_nat O (S m) = true <-> O = S mtrue = true <-> O = Om: natfalse = true <-> O = S mtrue = true <-> O = Oall: reflexivity.true = true -> O = OO = O -> true = truem: natfalse = true <-> O = S mall: discriminate.m: natfalse = true -> O = S mm: natO = S m -> false = truen', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = meq_nat (S n') m = true <-> S n' = m
See how the IH is quantified for all m.
n': nat
IH: forall m : nat, eq_nat n' m = true <-> n' = meq_nat (S n') O = true <-> S n' = On', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = meq_nat (S n') (S m) = true <-> S n' = S mn': nat
IH: forall m : nat, eq_nat n' m = true <-> n' = mfalse = true <-> S n' = On', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = meq_nat n' m = true <-> S n' = S mn': nat
IH: forall m : nat, eq_nat n' m = true <-> n' = mfalse = true <-> S n' = Oall: discriminate.n': nat
IH: forall m : nat, eq_nat n' m = true <-> n' = mfalse = true -> S n' = On': nat
IH: forall m : nat, eq_nat n' m = true <-> n' = mS n' = O -> false = truen', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = meq_nat n' m = true <-> S n' = S mn', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = meq_nat n' m = true -> S n' = S mn', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = mS n' = S m -> eq_nat n' m = truen', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = m
E: eq_nat n' m = trueS n' = S mn', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = m
E: S n' = S meq_nat n' m = truen', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = m
E: eq_nat n' m = trueS n' = S mn', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = m
E: n' = mS n' = S mreflexivity.n', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = m
E: n' = mS m = S mn', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = m
E: S n' = S meq_nat n' m = truen', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = m
E: S n' = S mn' = mreflexivity. Qed.n', m: nat
IH: forall m : nat, eq_nat n' m = true <-> n' = m
E: S n' = S m
H0: n' = mm = m
Booleans
We played with natural numbers quite a bit now. It's time to see other inductive types. We start with Booleans, which are even simpler.
Inductive bool := true | false.
Since bool is not recursive, the induction principle is a lot simpler.
In this case, it makes sense to talk about case anlysis instead.
Again, since it's an inductive type, the constructors are disjoint.
true <> falsediscriminate. Qed.true <> false
Cartesian product
We can define more types, such as the cartesian product.
We define product types inductively with one constructor taking two arguments.
Inductive prod X Y :=
| pair (x : X) (y : Y).Note how the constructor pair takes 4 arguments:
Namely types X and Y, and elements x : X and y : Y.
The first two arguments are real arguments that can be passed explicitly: types are first-class in Rocq.
Furthermore, types can appear at any position in arguments, unlike OCaml where the quantification is necessarily prenex.
Definition pair' X (x : X) Y (y : Y) := pair X Y x y.
Importantly, the type arguments have to be passed.
However, we can declare arguments as implicit using the Arguments
command by marking implicit arguments with braces {}
(sometimes called squigly brackets).
Arguments pair' {X} x {Y} y.When an argument is implicit, Rocq tries to infer what it has to be, based
on the other passed arguments. It involves a process called unification.
If we build the pair (O,O) then both types have to be nat and Rocq is
able to figure it out.
We can use the following command to force Rocq to print implicit arguments.
Set Printing Implicit.There we can see the nat that have been inferred.
We go back to not printing implicit arguments.
Unset Printing Implicit.We can define the first projection by pattern matching.
We mark arguments as implicit directly, but we could have used Arguments later
instead.
Definition fst {X Y} (p : prod X Y) : X :=
match p with
| pair _ _ x y => x
end.
This is just the tip of the iceberg and we shall see more interesting and complicated example of inductive types soon.
For now, let us focus on exercises. 😉