Week 1: Intro
- Author
Aurèle Barrière.
Inspiration from Software Foundations and a previous lecture by Nate Foster.
Types and functions
In this class, we'll use Rocq to write programs, and prove things about them! Rocq has three languages: one for commands (the “Vernacular”), one for terms (“Gallina”), and one for proof tactics (“Ltac”).
Let's start by defining a custom type in Gallina, an enumerated type.
We define the type day
, which has seven constructors: seven possible values for this type.
Inductive day : Type :=
| monday
| tuesday
| wednesday
| thursday
| friday
| saturday
| sunday.
So far this is just an enumerated type, like you would write an enum in C. But we'll see later that we can define much more complex types.
Now, let's write our first function in Gallina, with some pattern-matching. Note that functions have to be total, I have to provide a result for all possible values of the argument.
Definition next_weekday (d: day) : day :=
match d with
| monday => tuesday
| tuesday => wednesday
| wednesday => thursday
| thursday => friday
| friday => monday
| saturday => monday
| sunday => monday
end.
One way to check the result of executing our function is to use the vernacular command Compute
. Another way could be to extract our Rocq functions into OCaml or Haskell!
Another way is to prove that the execution of my function gives the expected result. Here we'll see our first interactive proof buffer, with an horizontal line. Above the line are our hypotheses (in this particular proof, there aren't any. Below the line is our proof goal.
next_weekday wednesday = thursdaynext_weekday wednesday = thursdayreflexivity. Qed.thursday = thursday
The simpl
tactic simplifies the aplication of a function by following its definition. And reflexivity
conclude when our goal is an equality were both sides are exactly the same.
Now, I want to prove that our function never returns a day that is part of the weekend. Let us start by defining which days are part of the weekend. This function takes a day and returns a boolean. I could define my own boolean type, just like I did for day
. But this is part of the standard library already. Let's look at its definition with Print
.
Now we can write our weekend
definition, returning a boolean. Note that in many cases, Rocq can do some type inference and figure out some types by itself, I could remove all of the type annotations below and the definition would still be accepted.
Definition weekend (d: day) : bool :=
match d with
| saturday | sunday => true
| _ => false
end.
Gallina is strongly typed, every term has a type. We can check these types with Check
. Of course, Rocq will reject ill-typed terms.
We are now ready for our proof, using the universal quantifier forall
to quantify over any possible input to the function. This theorem is proved by case analysis over the input d
. To do so, we use the tactic destruct
. We use -
to focus on the current sub-goal and structure our proofs.
forall d : day, weekend (next_weekday d) = falseforall d : day, weekend (next_weekday d) = falsed: dayweekend (next_weekday d) = falseweekend (next_weekday monday) = falseweekend (next_weekday tuesday) = falseweekend (next_weekday wednesday) = falseweekend (next_weekday thursday) = falseweekend (next_weekday friday) = falseweekend (next_weekday saturday) = falseweekend (next_weekday sunday) = falseweekend (next_weekday monday) = falseweekend tuesday = falsereflexivity.false = falseweekend (next_weekday tuesday) = falsereflexivity.false = falseweekend (next_weekday wednesday) = falsereflexivity.false = falseweekend (next_weekday thursday) = falsereflexivity.false = falseweekend (next_weekday friday) = falsereflexivity.false = falseweekend (next_weekday saturday) = falsereflexivity.false = falseweekend (next_weekday sunday) = falsereflexivity. Qed.false = false
So far, we only defined a simple enumerated type. But we can define more complex abstract data types. let's define natural numbers and polymorphic lists. Natural numbers use an unary representation, where each number is either 0 or the successor of another natural.
Inductive mynat : Type := | O | S (n:mynat). Inductive mylist (X:Type) : Type := | nil: mylist X | cons (x:X) (l:mylist X) : mylist X.
In fact, those definitions are already in the standard library. With Search
, we can even search for definitions related to these types.
Now let's introduce some more proof tactics. Here, we use rewrite
to use an equality in our hypotheses. Note that using rewrite <-
would rewrite the equality in the other direction.
forall n m : nat, n = m -> n + n = m + mforall n m : nat, n = m -> n + n = m + mn, m: natn = m -> n + n = m + mn, m: nat
H: n = mn + n = m + mreflexivity. Qed.n, m: nat
H: n = mm + m = m + m
In this proof, we use the keyword as
to name the new terms introduce by case analysis. In that case, we need to name the natural of which n
is a successor of.
forall n : nat, Nat.eqb (n + 1) 0 = falseforall n : nat, Nat.eqb (n + 1) 0 = falsen: natNat.eqb (n + 1) 0 = falsen: nat
E: n = 0Nat.eqb (0 + 1) 0 = falsen, n': nat
E: n = Datatypes.S n'Nat.eqb (Datatypes.S n' + 1) 0 = falsen: nat
E: n = 0Nat.eqb (0 + 1) 0 = falsereflexivity.n: nat
E: n = 0false = falsen, n': nat
E: n = Datatypes.S n'Nat.eqb (Datatypes.S n' + 1) 0 = falsereflexivity. Qed.n, n': nat
E: n = Datatypes.S n'false = false
Note that destruct
typically forgets entirely about what we did case analysis on. We can remember that term and what it's equal to with the keyword eqn:
.
forall b c : bool, (b && c)%bool = (c && b)%boolforall b c : bool, (b && c)%bool = (c && b)%boolb, c: bool(b && c)%bool = (c && b)%boolb, c: bool
Eb: b = true(true && c)%bool = (c && true)%boolb, c: bool
Eb: b = false(false && c)%bool = (c && false)%boolb, c: bool
Eb: b = true(true && c)%bool = (c && true)%boolb, c: bool
Eb: b = true
Ec: c = true(true && true)%bool = (true && true)%boolb, c: bool
Eb: b = true
Ec: c = false(true && false)%bool = (false && true)%boolreflexivity.b, c: bool
Eb: b = true
Ec: c = true(true && true)%bool = (true && true)%boolreflexivity.b, c: bool
Eb: b = true
Ec: c = false(true && false)%bool = (false && true)%boolb, c: bool
Eb: b = false(false && c)%bool = (c && false)%boolb, c: bool
Eb: b = false
Ec: c = true(false && true)%bool = (true && false)%boolb, c: bool
Eb: b = false
Ec: c = false(false && false)%bool = (false && false)%boolreflexivity.b, c: bool
Eb: b = false
Ec: c = true(false && true)%bool = (true && false)%boolreflexivity. Qed.b, c: bool
Eb: b = false
Ec: c = false(false && false)%bool = (false && false)%bool
Propositions and Propositional Logic
We said that every term has a type, what is the type of the statements we've been proving?
Rocq has a special kind of type, named Prop
to represent propositions, that we might try to prove. Note that not all propositions are true, they are just statements for which we may provide a proof. Propositions are a first-class entity in Rocq that can be defined, manipulated almost like any other type.
Definition false_prop: Prop := 3 = 2.3 = 23 = 23 = 2Abort. Definition plus_claim : Prop := 2 + 2 = 4.
We often define functions that create propositions. This defines properties of the arguments.
Definition is_three (n : nat) : Prop := n = 3.Definition injective {A B:Type} (f : A -> B) : Prop := forall x y : A, f x = f y -> x = y.
Now that we have propositions, we can do propositional logic. Let's see how we manipulate conjunctions, disjunctions and implications.
3 + 4 = 7 /\ 2 * 2 = 43 + 4 = 7 /\ 2 * 2 = 43 + 4 = 72 * 2 = 4reflexivity.3 + 4 = 7reflexivity. Qed.2 * 2 = 41 = 2 \/ 3 + 9 = 121 = 2 \/ 3 + 9 = 12reflexivity. Qed.3 + 9 = 12forall A B : Prop, A /\ B -> Aforall A B : Prop, A /\ B -> AA, B: Prop
H: A /\ BAexact HA. Qed.A, B: Prop
HA: A
HB: BAforall A B : Prop, A \/ B -> B \/ Aforall A B : Prop, A \/ B -> B \/ AA, B: Prop
H: A \/ BB \/ AA, B: Prop
HA: AB \/ AA, B: Prop
HB: BB \/ AA, B: Prop
HA: AB \/ Aexact HA.A, B: Prop
HA: AAA, B: Prop
HB: BB \/ Aexact HB. Qed.A, B: Prop
HB: BB
We can also have equivalences, which is in practice a conjunction of two implications.
forall A B : Prop, A /\ B <-> B /\ Aforall A B : Prop, A /\ B <-> B /\ AA, B: PropA /\ B <-> B /\ AA, B: PropA /\ B -> B /\ AA, B: PropB /\ A -> A /\ BA, B: PropA /\ B -> B /\ AA, B: Prop
H: A /\ BB /\ AA, B: Prop
HA: A
HB: BB /\ AA, B: Prop
HA: A
HB: BBA, B: Prop
HA: A
HB: BAexact HB.A, B: Prop
HA: A
HB: BBexact HA.A, B: Prop
HA: A
HB: BAA, B: PropB /\ A -> A /\ BA, B: Prop
H: B /\ AA /\ BA, B: Prop
HB: B
HA: AA /\ BA, B: Prop
HB: B
HA: AAA, B: Prop
HB: B
HA: ABexact HA.A, B: Prop
HB: B
HA: AAexact HB. Qed.A, B: Prop
HB: B
HA: AB
To use an implication, the tactic apply
allows us to match the conclusion of an hypothesis with the goal, and replace the goal with the premise of that implication. Note that we could also have made progress in the hypotheses instead of the goal using apply ... in ...
. In fact, a lot of tactics can work on either the goal or the hypotheses. We can do both forward and backward reasoning, and often even meet in the middle.
forall A B : Type, (A -> B) -> A -> Bforall A B : Type, (A -> B) -> A -> BA, B: Type
Himpl: A -> B
HA: AB(* apply Himpl in HA. *) apply HA. Qed.A, B: Type
Himpl: A -> B
HA: AA
Finally, negation is defined as implying False
. The False
proposition is a proposition with no constructors, so we cannot construct a proof of it.
forall P : Type, False -> Pforall P : Type, False -> Pcontradiction. Qed.P: Type
H: FalsePforall P : Prop, ~ P -> P -> Falseforall P : Prop, ~ P -> P -> FalseP: Prop
Hnot: ~ P
HP: PFalseP: Prop
Hnot: P -> False
HP: PFalseapply HP. Qed.P: Prop
Hnot: P -> False
HP: PP0 <> 10 <> 10 = 1 -> Falseinversion H. Qed.H: 0 = 1Falseforall P : Prop, P -> ~ ~ Pforall P : Prop, P -> ~ ~ Pforall P : Prop, P -> (P -> False) -> FalseP: Prop
H: P
H0: P -> FalseFalseapply H. Qed.P: Prop
H: P
H0: P -> FalsePforall P Q : Prop, (P -> Q) -> ~ Q -> ~ Pforall P Q : Prop, (P -> Q) -> ~ Q -> ~ Pforall P Q : Prop, (P -> Q) -> (Q -> False) -> P -> FalseP, Q: Prop
H: P -> Q
H0: Q -> False
H1: PFalseP, Q: Prop
H: P -> Q
H0: Q -> False
H1: PQapply H1. Qed.P, Q: Prop
H: P -> Q
H0: Q -> False
H1: PP
Finally, let's see how we can work with quantifiers, universal and existential. We've seen that intros
is useful when our goal is a forall
. When we have a forall
as an hypothesis, we can use apply ... with
to use it, or specialize
it.
forall (A : Type) (f : A -> A), (forall x : A, f x = f (f x)) -> forall y : A, f (f y) = f (f (f y))forall (A : Type) (f : A -> A), (forall x : A, f x = f (f x)) -> forall y : A, f (f y) = f (f (f y))(* specialize (Hinvol (f y)). exact Hinvol. *) apply Hinvol with (x:= f y). Qed.A: Type
f: A -> A
Hinvol: forall x : A, f x = f (f x)
y: Af (f y) = f (f (f y))
For existential quantifiers, we can use the exists
tactic to provide a witness. And we can destruct
an existential hypothesis to exhibit the witness.
forall x : nat, exists y : nat, y = xforall x : nat, exists y : nat, y = xx: natexists y : nat, y = xreflexivity. Qed.x: natx = x(exists x : nat, x + 2 = 5) -> exists y : nat, 3 * (y + 2) = 15(exists x : nat, x + 2 = 5) -> exists y : nat, 3 * (y + 2) = 15H: exists x : nat, x + 2 = 5exists y : nat, 3 * (y + 2) = 15x: nat
Hx: x + 2 = 5exists y : nat, 3 * (y + 2) = 15x: nat
Hx: x + 2 = 53 * (x + 2) = 15reflexivity. Qed.x: nat
Hx: x + 2 = 53 * 5 = 15