lesson_1
Lesson 1: Type Theory as a Mathematical Language
From mathcomp Require Import all_ssreflect.
Set Implicit Arguments.
Unset Strict Implicit.
Unset Printing Implicit Defensive.
Interaction model:
Theorem x_implies_x : ∀ P, P → P.
Proof. by auto. Qed.
Proof. by auto. Qed.
Basic Coq Commands:
Definition / Theorem:
- Interactive form:
- Definitional form:
Theorem x_implies_x_v1 : ∀ P, P → P.
Proof. by auto. Qed.
Theorem x_implies_x_v2 P : P → P.
Proof. by auto. Qed.
Definition x_implies_x_v3 : ∀ P, P → P := fun _ x ⇒ x.
Definition x_implies_x_v4 P : P → P := fun x ⇒ x.
Proof. by auto. Qed.
Theorem x_implies_x_v2 P : P → P.
Proof. by auto. Qed.
Definition x_implies_x_v3 : ∀ P, P → P := fun _ x ⇒ x.
Definition x_implies_x_v4 P : P → P := fun x ⇒ x.
It is possible to annotate arguments with types, so we can write:
Definition x_implies_x_v4' (P : Prop) : P → P := fun x ⇒ x.
Print:
Print x_implies_x_v1.
Print x_implies_x_v4.
Print x_implies_x_v4'.
Print bool.
Print nat.
Print list.
Print x_implies_x_v4.
Print x_implies_x_v4'.
Print bool.
Print nat.
Print list.
About:
About x_implies_x_v1.
Print path.
About path.
Print path.
About path.
Check:
Check path.
Check (path leq 0 [:: 1; 2; 3]).
Check true.
Check (true : bool).
Check (path leq 0 [:: 1; 2; 3]).
Check true.
Check (true : bool).
Search:
Search (_ ≤ _ + _).
Documentation
Section:
Section Example.
Variables (n : nat).
Lemma zero_is_left_id_for_plus : 0 + n = n. Proof. by []. Qed.
Lemma zero_is_right_id_for_plus : n + 0 = n. Proof. by apply/eqP; elim: n. Qed.
About zero_is_right_id_for_plus.
End Example.
About zero_is_right_id_for_plus.
Variables (n : nat).
Lemma zero_is_left_id_for_plus : 0 + n = n. Proof. by []. Qed.
Lemma zero_is_right_id_for_plus : n + 0 = n. Proof. by apply/eqP; elim: n. Qed.
About zero_is_right_id_for_plus.
End Example.
About zero_is_right_id_for_plus.
Coq's Type theory and Functional Programming
What is a type?
p : T
p ∈ T
.
x₁ ... xₙ
such that Γ := (x₁ : T₁) ... (xₙ : Tₙ)
.
Γ ⊢ p : T
Γ ⊢ p₁ : T₁ Γ ⊢ p₂ : T₂ ───────────────────────────── Γ ⊢ ⟨ p₁ , p₂ ⟩ : T₁ × T₂
Γ , p , T
belongs to the ⊢ relation. This process can be
decidable or not.
Proofs as types
Γ ⊢ p : T
- a proof of "P₁ ∧ P₂" is a tuple < p₁, p₂ > where pᵢ is a proof of i.
- a proof of "P₁ ∨ P₂" is a pair <s,p> where if s (a boolean) is true
then p is a proof of P₁, else p is a proof of P₂.
- a proof of "P -> Q" is a function that takes as input a proof of
P and returns a proof of Q.
- a proof of "∀ x : S, ϕ(x)" is a function that converts an element
s of S into a proof of "ϕ(s)"; note the similarity with the interpretation
of implication !
- a proof of "∃ x : S, ϕ(x)" is a tuple <w,p> where w is an element of S and p is a proof of "ϕ(w)"
Coq's programs and types
The type universe
A type inside a type?
Check (3 : nat : Type).
Fail Check (nat : nat).
Fail Check (3 : Type).
Fail Check (nat : nat).
Fail Check (3 : Type).
Type formation rules
Γ ⊢ P : Prop Γ, (x : P) ⊢ Q : Prop ──────────────────────────────────── Γ ⊢ forall (x : P), Q : Prop
Prop vs bool
Gallina, Coq's programming language
Variables
(x : T) ∈ Γ ───────────── Γ ⊢ x : T
Creating functions / reasoning under hypotheses
Γ, (x : T) ⊢ e : U Γ ⊢ T : Prop Γ, (x : T) ⊢ U: Prop ───────────────────────────────────────────────────────────── Γ ⊢ λ (x : T) . e : forall (x : T), U
Check (fun x ⇒ x).
Check (fun (x : nat) ⇒ x).
Check (fun (x : nat) ⇒ erefl x).
Check (fun (x : nat) ⇒ x).
Check (fun (x : nat) ⇒ erefl x).
Function application / modus-ponens
Γ ⊢ f : ∀ (x : T), U Γ ⊢ e : T ───────────────────────────────────── Γ ⊢ f e : U[e/x]
Check ((fun (x : nat) ⇒ erefl x) 3).
In the case where the type doesn't depend on the argument (non-dependent case), we have:
The above rules complete the presentation of the Calculus of Constructions!
While we are missing some details related to Type, it is indeed
remarkable how minimal it is, for the amount of logical power it packs.
You may wonder "but what about the logical primitives?" "Where is negation, where
is or? Where is bool?"
That's a great question, and indeed so far we have been using some primitives that are not
part of the above presentation. The reason is that types such as bool,
and connectives such as or, or negation, can be encoded in the calculus we presented.
For example, not and bool can be defined by using their "elimination principle":
Γ ⊢ f : T → U Γ ⊢ e : T ─────────────────────────── Γ ⊢ f e : Uwe can combine some rules to get:
Γ ⊢ f : T → U Γ ⊢ e : U → V ───────────────────────────────── Γ ⊢ λ (x : T) , e (f x) : T → Vwhich is the familiar modus-ponens rule.
Inductive Data Types
Definition imp_not (P : Prop) := ∀ (R : Prop), P → R.
Definition imp_bool (P : Prop) := P → P → P.
Definition imp_bool (P : Prop) := P → P → P.
The other connectives are encoded similarly. However, these encoding are
pretty cumbersome to work with, and moreover they have bad computational
properties.
Thus, the original Coq Calculus is the extension of the core with
inductively defined "data types", similar to the "algebraic data
types" found in languages such as OCaml, Haskell, or Rust. Coq's
inductives tho have some unique features due to the proof-theoretic
nature of Coq which we will explore in the next lesson.
Today, we will focus on more "vanilla" inductive data types.
While so far we have been using Gallina to define programs, inductives
are truly "pieces of data". For example, we can define the natural numbers
in Coq in the following way:
Inductive definition
Inductive natural : Type :=
| o : natural
| s : natural → natural.
| o : natural
| s : natural → natural.
Let's examine the definition carefully: the first line declares to Coq natural as
a type, thus extending the typing rules to admit natural : Type.
The second part of the declaration is what we call "constructors", that is to say, ways
to build a natural number. The first constructor is the constant `O` , which is by itself
a natural, and the second constructor is S, which takes a natural. So, if x : nat, then S x : nat.
Coq's typing rules are extended so o : natural, and x : natural ⊢ s x : natural.
Constructors can be seen as "tags" that represent different cases, so their
name is a user choice. We can as well write:
Inductive natural' : Type :=
| zero : natural'
| succ : natural' → natural'.
| zero : natural'
| succ : natural' → natural'.
or even
Inductive weird_natural : Type :=
| zero' : weird_natural
| succ' : weird_natural → weird_natural
| weird : weird_natural → weird_natural → weird_natural.
| zero' : weird_natural
| succ' : weird_natural → weird_natural
| weird : weird_natural → weird_natural → weird_natural.
The key point is that this specification fully determines what the elements of the
natural type are. For the first case, we have that if x : natural, x
has to be either O or S x', where x' is another natural.
Coq allows us to define quite arbitrary inductives, and they don't necessarily have
be possible to inhabit. For example, this is a valid definition:
Inductive oups : Type :=
| not_possible : 0 = 1 → oups.
| not_possible : 0 = 1 → oups.
The elements of opus are all the proofs of 0 = 1, and as we know Coq
is consistent, there is no such proof. So while we could define the data type, we
will never be able to build one of its elements.
So inductives extend the "programs are proof" motto to "data is proof". Inductive data
types one of the key points in the success of Coq as a tool, as they allow us to model
mathematical universes pretty precisely, while at the same time keeping the guarantee
that we can't build some data that would lead us to inconsistency.
This is not so easy, given that you may have noticed that for example the natural
is recursive in nature. Indeed, Coq places some extra conditions on which
inductives are admissible. Most common one is called "the positivity condition".
Dually, when analyzing / consuming some data that is recursive, now Coq has to check that
the function doing this is well-founded and thus it will terminate.
Provided some element of an inductive data type, we want to be able to analyze it
in base of its declared constructors. For example, given a natural number, we
may want to compute if the number is even. Or if our data represents some possible
behaviors of a system, equivalently we may want to do a proof by case analysis.
Coq supports by means of the match statement:
Case analysis
Fixpoint even (n : nat) : bool :=
match n with
| O ⇒ true
| S 0 ⇒ false
| S (S n) ⇒ even n
end.
Compute (even 10, even 9).
match n with
| O ⇒ true
| S 0 ⇒ false
| S (S n) ⇒ even n
end.
Compute (even 10, even 9).
Note that instead of Definition, here we use the Fixpoint keyword, which
does tell Coq that our definition is recursive. Coq will check that the definition
indeed does terminate. If we try a definition that Coq can't see terminating, Coq
will complain:
Fail Fixpoint loop (n : nat) : bool := loop n.
We can disable this check and get a proof of false
#[bypass_check(guard)]
Fixpoint loop (n : nat) : False := loop n.
Fixpoint loop (n : nat) : False := loop n.
We can do case analysis for the bad datatype too:
Search 0.
Definition oups_is_bad (o : oups) : False :=
match o with
| not_possible p0eq1 ⇒ O_S _ p0eq1
end.
About O_S.
Print not.
Definition oups_is_bad (o : oups) : False :=
match o with
| not_possible p0eq1 ⇒ O_S _ p0eq1
end.
About O_S.
Print not.
Logic as Data
Module MyDefs.
Inductive False :=.
Definition not P := P → False.
Inductive and (P Q : Prop) :=
| And : P → Q → and P Q.
Inductive or (P Q : Prop) :=
| Left : P → or P Q
| Right : Q → or P Q.
Inductive exist (A : Type) (P : A → Prop) : Type :=
ex : ∀ x : A, P x → exist P.
End MyDefs.
Inductive False :=.
Definition not P := P → False.
Inductive and (P Q : Prop) :=
| And : P → Q → and P Q.
Inductive or (P Q : Prop) :=
| Left : P → or P Q
| Right : Q → or P Q.
Inductive exist (A : Type) (P : A → Prop) : Type :=
ex : ∀ x : A, P x → exist P.
End MyDefs.
Specifications in the Coq's Mathematical Universe
Record cmagma :=
{ A :> Type
; op : A → A → A
; op_comm : commutative op
}.
{ A :> Type
; op : A → A → A
; op_comm : commutative op
}.
As far as we are concerned, this is just a piece of data that packs
a type, an operator, and an axiom of the operator. Can we construct such
a structure with a type that is not void?
The answer is yes:
Print cmagma.
Definition nat_magma := {| A := nat; op := addn; op_comm := addnC |}.
Check (3 : nat_magma).
Definition nat_magma := {| A := nat; op := addn; op_comm := addnC |}.
Check (3 : nat_magma).
We will dwell into this more in the next lesson, but in general
we can understand elements of inductive types like elements of a model of
Coq's coherent mathematical universe.
So far, we have been using "computation" and equality in a quite informal sense.
The notion of computation in Coq mainly corresponds to the notion of computation
for the lambda calculus, that is to way, provided a function f : U → T and
a term x : T, the application f u : T will compute by substituting u for
the variable in the definition of f.
There are several strategies in terms of evaluation strategies, but the main tricky part
is that case analysis can't progress when a variable is in a match position:
Computation and equality
Eval cbv in (fun b ⇒ orb b true).
Unset Printing Notations.
Eval cbv in (fun (b : bool) ⇒ orb false true).
Set Printing Notations.
Unset Printing Notations.
Eval cbv in (fun (b : bool) ⇒ orb false true).
Set Printing Notations.
The first result tells us that the if is waiting on b to get a value,
and thus we can't progres as we do in the second case. This is logical and expected.
However, it is well-known that in the presence of multiple parameters, we
have to bias evaluation.
Something we skimmed over (on purpose) so far was the question of equality of Coq programs.
But if you look at the typing rules, it turns out that Coq needs to decide when typing when
two types and programs are equal.
Usually, we say that t1 is convertible to t2 if there is a term t such that t1 and t2 will evaluate to it, that is to say:
Thus, when typing, Coq will consider two types equal when they are convertible:
Equality in type theory is also an advanced research topic, with many possible
definitions. Coq defines "user-level" or "propositional" (as opposed to computational)
equality as a data type
∃ t, t₁ ↦ t ∧ t₂ ↦ t ────────────────────── t₁ ≈ t₂for example true || false ≈ true, 3 + 3 ≈ 6, and n.+1 ! ≈ n.+1 × n!, etc...
T ≈ U Γ ⊢ p : T ────────────────────── Γ ⊢ p : UHaving convertibility in the type rules as a built-in is extremely useful and another reason for the success of Coq. However, there is a great tension between equality and computation, as we will short illustrate.
Equality, briefly
Print eq.
When combined with the conversion typing rule, we have that all terms that
are convertible are equal. However the converse doesn't hold, as we can strictly
equate more terms propositionality than by conversion.
We will see more examples in the next lesson, but the following example illustrates the
problem:
Section EqComp.
Variable (n : nat).
Check (erefl : 0 + n = n).
Fail Check (erefl : n + 0 = n).
Print Nat.add.
End EqComp.
Variable (n : nat).
Check (erefl : 0 + n = n).
Fail Check (erefl : n + 0 = n).
Print Nat.add.
End EqComp.
In the second case, computation blocks. That may be surprising but
it is due to + checking the
left argument first. As it is a variable, it can't decide which case it is.
Another way to understand this mismatch is by thinking of computation as
directed vs equality as undirected.
Despite its shortcoming, computation inside Coq is extremely useful to do proofs; when our term
is variable-free, we will get a normal form, and that has been used extensively
to automate large proofs.
Moreover, we can write algorithms inside Coq and prove their properties, then run them, and obtain
a proof over the result. To do a proof by computation, we just need to tell Coq that our proof term
is the equality constructor, erefl:
Proofs by computation
Definition plus33eq6 : 3 + 3 = 6 := erefl.
Definition list_app : [:: 3; 5; 6] = rev (rev [:: 3; 5; 6]) := erefl.
Definition list_app : [:: 3; 5; 6] = rev (rev [:: 3; 5; 6]) := erefl.
Coq's front-end
Notations and Implicit Arguments
Theorem or_commutative P Q : P ∨ Q → Q ∨ P.
Set Printing All. by tauto. Qed.
Theorem eq_comm (T : Type) (x y : T) : x = y → y = x.
Proof. by []. Qed.
About eq_comm.
Set Printing All. by tauto. Qed.
Theorem eq_comm (T : Type) (x y : T) : x = y → y = x.
Proof. by []. Qed.
About eq_comm.
Another important feature that users face a lot are implicit arguments.
In many cases, like in equality, the first argument (base type of the
equality) will be uniquely determined by the next argument. Thus, Coq
allows us to declare this argument as "implicit", and a hole will be
inserted for it automatically. Implicits are marked by using braces on
the argument declaration, or by a explicit command.
Thus, these two expressions are equivalent eq 3 and @eq nat 3.
Coq has a quite advanced type inference engine. By virtue of Coq types containing
terms, Coq's type inference engine is also a program synthesis engine on its own.
Type inference is very important as Coq's kernel does require every lambda abstraction
to be annotated with the type in order to type-check it.
Moreover, it is often common to omit types of arguments for better readability and
let Coq figure out using constraints and unification, and the first phase of implicit
argument resolution will produce many "type holes" to be filled by the inference engine.
While it is out of scope for this tutorial, making an effective use of Coq's type
inference engine is essential to achieve an efficient proof style in the real world, both
at the user level and at the tactic level.
Among others, Coq's type inference engine support bidirectional inference, coercions,
type classes, type-directed synthesis of records, and higher-order unification.
Type inference
Unset Printing All.
Fail Lemma foo : x = x.
Lemma foo' x : x = 1.
Fail Lemma foo : x = x.
Lemma foo' x : x = 1.