Lesson 1: Coq Core Topics and Tactics
From mathcomp Require Import all_ssreflect.
Set Implicit Arguments.
Unset Strict Implicit.
Unset Printing Implicit Defensive.
Definition sorry {T} : T. Admitted.
Interaction model:
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:
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:
Check:
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?
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
Function application / modus-ponens
Γ ⊢ f : ∀ (x : T), U Γ ⊢ e : T ───────────────────────────────────── Γ ⊢ f e : U[e/x]
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
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 definitions
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:
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:
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:
We can disable this check and get a proof of False
We can do case analysis for the bad datatype too to obtain a proof of False:
Definition oups_is_bad (o : oups) : False :=
match o with
| not_possible p0eq1 ⇒ O_S _ p0eq1
end.
About O_S.
match o with
| not_possible p0eq1 ⇒ O_S _ p0eq1
end.
About O_S.
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.
Note that the above definitions match very closely our semantic definition about
what a proof is. For example, a proof of or P Q is either a proof of P, tagged
with the Left constructor, or a proof of Q, tagged with Right
A very interesting and powerful use case of inductive definitions is building
models of our specifications and logical theories.
Imagine we have the axioms for a commutative magma, that is to say, a type
with a binary endo-operator which happens to be commutative.
We can specify that as:
Specifications in the Coq's Mathematical Universe
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
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
Set Printing All.
Theorem or_commutative P Q : P ∨ Q → Q ∨ P.
Proof. by tauto. Qed.
Unset Printing All.
Theorem eq_comm (T : Type) (x y : T) : x = y → y = x.
Proof. by []. Qed.
About eq_comm.
Unset Printing All.
Theorem or_commutative P Q : P ∨ Q → Q ∨ P.
Proof. by tauto. Qed.
Unset Printing All.
Theorem eq_comm (T : Type) (x y : T) : x = y → y = x.
Proof. by []. Qed.
About eq_comm.
Unset Printing All.
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
We will now move to learn the main Coq tactics and different proof
styles using them. Before starting, let's quickly summarize the core Coq
syntax we have just seen:
The canonical reference is the Coq manual https://coq.inria.fr/refman/
Core typing judgment:
p has type T under hypothesis Γ . Checking this is the main purpose of Coq's kernel.
The type A → B is notation for ∀ (_ : A), B.
In the previous lesson, we did learn what it means to be a proof. We can now try to prove some simple
logical statements writing their corresponding program:
Coq's Syntax Cheat-sheet:
Γ ⊢ p : T
Vernacular Commands:
id ::= $string thm ::= Definition | Theorem | Lemma | Example command ::= $thm $id $argument* (: $type)? . Proof.? $tactic*. $end . | $thm $id $argument* (: $type)? := $term . | Fixpoint $id $argument+ (: $type)? := $term. | Print $id | About $id | Check $id | Search $search_options argument ::= $id | ( $id : $type ) end ::= Qed | Defined | Admitted
Terms and types:
kind ::= Prop collection of all "formulas" | Type collection of all "types" term ::= $kind | $id variable or constant | forall $argument+ , $type Pi Type (or type function) | fun $argument+ => $term regular function | $term $term application | match $term with $branch* end. pattern matching branch ::= | $id+ => $term type ::= $term such that "$term : $kind"
Inductives
inductive_decl ::= Inductive $id $argument* (: $type)? ::= $constructor_decl* constructor_decl ::= | $id : $type where $type has to belong to the inductive being declared
Some core defined datatypes:
- bool: Type of booleans (either true or false) Some boolean ops: a && b, a || b, a ==> b, ~~ b.
- nat: Type of Peano Numbers (either O or n.+1) Some nat ops: a + b, a - b, a - b, a ^ b, a %% b, a /% b.
- option A: Either Some x , with x : A or None
- list A: Lists of type A Some list ops: l ++ m, map f l, filter p l.
Some core defined logical connectives:
- x = y: equality
- True, False: Basic propositions
- ¬ P : negation, defined as P → False
- P ∧ Q: conjunction
- P ∨ Q: disjunction
- ∃ (x : T), P x: exists (where P a predicate on values of type T
Writing proof terms, tactics, and the proof cycle:
Definition logic1 P Q : P → Q → P := sorry.
Definition logic2 (P : Prop) : P → ¬ ¬ P := sorry.
Definition nat_inv : ∀ n, n = 0 ∨ ∃ m, S m = n := sorry.
Definition forall_dist T P Q :
(∀ (x : T), P x ∧ Q x) →
(∀ (x : T), P x) ∧ (∀ (x : T), Q x) := sorry.
Definition logic2 (P : Prop) : P → ¬ ¬ P := sorry.
Definition nat_inv : ∀ n, n = 0 ∨ ∃ m, S m = n := sorry.
Definition forall_dist T P Q :
(∀ (x : T), P x ∧ Q x) →
(∀ (x : T), P x) ∧ (∀ (x : T), Q x) := sorry.
Writing proof terms by hand this way quickly turns into a cumbersome
endeavor, as writing proof terms is not conceptually very close to
the way humans think of formal proof steps.
Additionally, Coq is a direct descendent from the LCF family of
provers, which are built from a different philosophy: their kernel
provides a set of "tactical", which are meant to represent human proof
steps. In this setting, users start from a "goal", and apply tactics
to refine the goals until the end has been reached. Such tactics are trusted, and soundness is
kept thanks to some abstraction mechanism in the host language.
For both this reasons, Coq quickly gained support for tactics, which in Coq's case,
are term-synthesis procedures. In Coq's case, the initial goal will be
the type or proposition we want to build a proof for, and we will use tactics
to gradually built the term. Goals correspond to holes in the proof, and we
are done when a complete program for the target goal has been built.
Tactics simply user life considerably, as in non-trivial cases,
writing low-level proof terms quickly becomes a very complex and
prone-to-error task.
The final step of this "proof cycle" is sending the newly-built terms
to kernel, as to ensure the term is really a proof of the initial goal.
A typical example of using a tactic is to prove a theorem of the
form A ∧ B ; we can use the split tactic, which will apply
the introduction rule (or constructor) for and and produce 2 separate
goals A and B.
Summarizing:
1. user states a particular proposition to be proved, submit to Coq
this proposition of "goal"
2. if the proposition itself can be typed (has type Prop or Type), Coq will
start the interactive mode, allow tactics to be enter
3. users can then provide a partial term, or call a
tactic to synthesize one for them
4. Coq will provide feedback on the newly submitted term, and either
produce an error, or update the goal and go back to step 3.
5. If there is no goal, we are done; we will close out proof with
the `Qed` command and Coq will re-check the final
proof as to catch any possible bugs with tactics.
Let's play a bit with tactics to prove a simple fact:
Theorem forall_dist' T P Q :
(∀ (x : T), P x ∧ Q x) →
(∀ (x : T), P x) ∧ (∀ (x : T), Q x).
Proof. Admitted.
(∀ (x : T), P x ∧ Q x) →
(∀ (x : T), P x) ∧ (∀ (x : T), Q x).
Proof. Admitted.
Proofs and telescopes: moving things around
This process can often feel like assembling a puzzle! And indeed, it is
in a sense very close to it.
But a key consideration of our puzzle is that some pieces do depend on others.
Coq is often called a "dependently-typed programming language", as indeed,
types can depend on terms. For example, consider the following type:
∀ (x y : nat) (x_eq_y : x = y) P (px_true: P x), P y
as we can see, px_true does depend on P and x. So we have no problem
in shuffling our statement like this:
∀ (x y : nat) P (px_true: P x) (x_eq_y : x = y), P y
however, we cannot do this, as then Coq will complain P is not known:
∀ (x y : nat) (px_true: P x) P (x_eq_y : x = y), P y
Thus, we working with assumptions, we must be aware of the dependencies between
hypothesis!
In fact, a large part of building a mechanized proof effectively is
relies on having the proper tools for the management of context and
assumptions, both locally and globally. This process is not an exact
science, and a few "styles" can be found in the wild.
Styles of proof writing and management greatly differ in several
accounts, such as:
In this lesson, we will make a quite opinionated choice, and select ssreflect
as our proof language of choice. ssreflect and the mathematical component library
were built for the development of large-scale mathematical proofs, and favor a highly-modular,
robust, and efficient style. Moreover, the maturity of the proof language is quite
high.
The ssreflect proof language is quite different from the one loaded by default from
the Coq prelude, and often misunderstood, as many of its design rationales
are maybe not documented in the most public places. In particular, I would highlight
the following features:
In particular, I'd dare to say that point one corrects a design flaw of Coq, which will
be discussed more in-depth in the case analysis section.
The first thing that the ssreflect proof language provides a set of "tacticals" operators, : and ⇒
which will allow tactics to perform context manipulation.
Usually, our starting goal will have the following shape:
Note how in this case, each assumption (and the terminal Goal) does depend on all the previous one.
We can place all the hypothesis in the assumption context using move⇒ hyp1 ... hypn, which
results in a goal:
note that this is equivalent to synthesizing a term fun hyp1 ... hypn ⇒ _ , where _ is a hole.
We can do move: hyp1 ... hypn to revert the goal to its original state.
We refer to ⇒ and : as operators as indeed they always appear together with a tactic:
When a tactic appears alone, for example move, it usually acts on the top of the goal / stack.
A special case is rewrite which can be seen as operating over the whole goal at a time.
Note that these tacticals are not tactics proper in the sense that just the arrow and the colon can just operate on the assumptions, we still
need a tactic to operate in the goal. It is very important to keep this "polarity" in
mind when doing proofs with math-comp. Usually, the operations that we can do on the assumptions are
the dual of the ones we can do in the conclusion.
We will briefly describe what $intro_pattern and $gen_pattern are, for a complete reference
you can consult the ssreflect reference manual, distributed with Coq.
An $intro_pattern is a list of operations to perform on the top of stack, we have the following cases:
For the purposes of this tutorial, we will consider a $gen_pattern as a list of terms; whose types will be placed
at the top of the goal, right to left.
As you will see, while the patterns look a bit cryptic, they will allow us for very compact and efficient context management.
A last thing to consider before starting to look into the tactics is whether we
are proving in "forward" or "backwards" style:
Of course, in many instances, we use a combination of the two styles.
∀ (x y : nat) (x_eq_y : x = y) P (px_true: P x), P y
∀ (x y : nat) P (px_true: P x) (x_eq_y : x = y), P y
∀ (x y : nat) (px_true: P x) P (x_eq_y : x = y), P y
- robustness to change: proofs are living objects, and often subject to continuous refactoring and refinement. Different styles provide different trade-offs in this aspect
- verbosity: some proof styles optimize for the writer, some others for the reader of the proof
- efficiency: some tactics and styles can be orders of magnitude faster
- automation: some proof styles favor highly "automated" styles, whereas others follow what's written very closely.
- principled "strict" approach to telescope management: ssreflect doesn't allow to perform implicit ill-formed telescope / context manipulation. If the user desires to do, they must indicate it explicitly.
- efficient assumption processing: ssreflect favors to process the assumptions just as they appear, for example, instead of adding an equality to the context, we may as well just rewrite with it and dispose the same moment we see it; similarly for assumptions that need case analysis. Moreover, this approach minimizes the amount of names that user needs to assign, creating more robust proofs.
- strict naming policy: all objects introduced in the proof context must be explicitly named, or forbidden to be referenced by name.
- efficient use of space: ssreflect favor a 80-column writing style, and a reasonably compact proof writing style, getting closer to human-workflow
- separation of implementation and specification: this usually results in more robust proofs. For example, it is not allowed to depend on the enumeration of a finite type, but only on some permutation of it.
- orthogonality / compositionality: ssreflect tries to minimize the amount of tactics, and tries to ensure they compose properly.
The assumption / telescope stack
========================================================================= forall (x : T1) (y : T2 x) ... (z : Tn x y ...), Goal x y ... z
hyp1 : T1 hyp2 : T2 hyp1 ... hypn : Tn hyp1 hyp2 ... ============== Goal hyp1 hyp2 ... hypn
- tac⇒ $intro_pattern this form will run tac , then apply the $intro_pattern to the
elements in the goals.
- tac: $gen_pattern this form will push the elements of $gen_pattern to the goal, then run tac.
Introduction and generalization patterns
- $id: if a name is there, the hypothesis will be introduced in the context with name $id (if such name is free)
- [pat | ... | pat]: this will tell Coq to try to destruct the hypothesis if it is an inductive, it is similar to doing case. If the hypothesis is not an inductive, it errors. For equality, it will try to run injection
- $intro_pattern | ... | $intro_pattern: when tac produces several goals, this will apply each intro_pattern to the goal. Cardinalities have to match.
- → or <-: this will try to rewrite the rest of the goal with the hypothesis, if it is an equality.
- { id }: this will clear the id from the list of hypothesis
- /$id: this will try apply to the top of goal the view $id (more on views later)
- //: this will try to solve the goal with the done tactic.
- /=: this will call simpl to simplify the goal.
Moving backwards, or forward?
- forward proving is usually done in mathematics, and it is when we transform the hypothesis until we obtain the goal
- a backwards style of proving is when we transform the goal until it is one of the hypothesis.a
Core tactics
move: Placeholder tactic
by: Closing proofs
apply: using hypothesis and constants
Lemma move_ex1 : nat → nat → nat.
Proof. by move⇒ n1 n2; apply: n2. Qed.
Print move_ex1.
Lemma move_ex2 (fn : nat → nat) : (nat → nat).
Proof. by move: fn; apply. Qed.
Print move_ex2.
Proof. by move⇒ n1 n2; apply: n2. Qed.
Print move_ex1.
Lemma move_ex2 (fn : nat → nat) : (nat → nat).
Proof. by move: fn; apply. Qed.
Print move_ex2.
Note that actually apply: name is really the composition of :
with apply, which acts on top of the stack!
So the previous example will actually first introduce fn at the top of the stack,
then apply will try to use it. This is akin to how RPN stack calculators work!
While unusual, this is pretty efficient when writing proofs, and moreover, composes pretty well with the
goal/stack manipulation operators.
Lemma apply_ex1 n y (H : n = 3 → y = 4) (U : n = 1 + 2) : 4 = y.
Proof. by apply: esym; apply: H; apply: U. Qed.
Proof. by apply: esym; apply: H; apply: U. Qed.
A more compact form is (exact is basically an alias for by apply)
Note that as you get more familiar with the system, and read more
high-quality proofs, you will discover countless ways to do things
more efficiently. Coq can of course solve this goal automatically:
have: stating partial results
- have $pat := thm arg1 ... argn. will perform lemma instantiation,
where thm is an already existing object which can take n (or more)
arguments. This is also specially useful to debug apply when it goes wrong.
- have $pat : T. will start a proof for the intermediate lemma T, then add H : T to the assumptions of the main goal.
Lemma have_ex1 n y (H : n = 3 → y = 4) (U : n = 1 + 2) : 4 = y.
Proof. Admitted.
Lemma have_ex2 n y (H : n = 3 → y = 4) (U : n = 1 + 2) : 4 = y.
Proof. Admitted.
Proof. Admitted.
Lemma have_ex2 n y (H : n = 3 → y = 4) (U : n = 1 + 2) : 4 = y.
Proof. Admitted.
rewrite replacing equals by equals
- rewrite {$occ_list}H: where $occ_list is a list of positions
- rewrite [$rew_pat]H: where $rew_pat is a pattern of the subterm to rewrite
Lemma rewrite_ex1 m : m + 0 = m.
Proof. Admitted.
Lemma rewrite_ex2 m n p : m + n + p + m = m + m + n + p.
Proof. Admitted.
Proof. Admitted.
Lemma rewrite_ex2 m n p : m + n + p + m = m + m + n + p.
Proof. Admitted.
We will see some more rewrite examples along the tutorial.
As we have discussed, a proof by case analysis in Coq corresponds to a
program that does pattern matching. Similarly, an induction proof will usually
do case analysis + well founded recursion.
A particularity of case analysis and induction is that these are
scoped operations. Let's see some examples:
case and elim: case analysis and induction
Lemma case_1 b1 b2 : b1 = b2 → b1 && b2 = b1.
Proof. Admitted.
Lemma case_2 T (x y : T) : x = y → y = x.
Proof. Admitted.
Lemma case_3 P Q : P ∧ Q → Q.
Proof. Admitted.
Lemma case_4 T P Q : P =1 Q → (∃ (x : T), P x) → ∃ (x : T), Q x.
Proof. Admitted.
Proof. Admitted.
Lemma case_2 T (x y : T) : x = y → y = x.
Proof. Admitted.
Lemma case_3 P Q : P ∧ Q → Q.
Proof. Admitted.
Lemma case_4 T P Q : P =1 Q → (∃ (x : T), P x) → ∃ (x : T), Q x.
Proof. Admitted.
Getting the scope or invariant right is usually the most challenging part of
case analysis and induction.
In general, we can safely state that the first rule of induction and
proofs is to avoid induction as much as possible!
Induction has bad dependency properties due to both depending on the
shape of the current goal and the shape of the object we are inducting
over.
Moreover, it is very often the case that a quite general lemma can be
proven by induction, and the rest of the theory can use it. Let's prove some statements by induction:
Lemma elim_1 m n p : m + n + p = m + (n + p).
Proof. Admitted.
Lemma size_map T U (f : T → U) l : size (map f l) = size l.
Proof. Admitted.
Proof. Admitted.
Lemma size_map T U (f : T → U) l : size (map f l) = size l.
Proof. Admitted.
Hint: prove an auxiliary lemma
Domain-specific tactics
- constructor (variants split and ∃): these will try to apply the constructor for a goal that is an inductive
- auto: performs automatic proof search using a database
- intuition: solver for intuitionistic predicate calculus
- lia,lra: arithmetic / simplex tactics