Course4: Advanced dependent types
Preliminaries
Wrap up on basic dependent types
A dependent type of nat_or_bool
Definition nat_or_bool (b:bool) : Type :=
if b then nat else bool.
An example of value in this type.
Definition value_nat_or_bool (b:bool) : nat_or_bool b :=
match b return (nat_or_bool b) with
| true ⇒ 0
| false ⇒ false
end.
Its type is not bool → nat_or_bool:
it is a dependent type, that is the return type
depends on the value of the input:
Check value_nat_or_bool.
value_nat_or_bool
: ∀ b : bool, nat_or_bool b
This match b ... is roughly equivalent to if b then 0 else false
but writing a if here would not give Coq enough details.
Even putting the explicit output type isn't enough, we add to use
an extra syntax return ... to help Coq.
Let us "program" the desired type, starting from the
number n. The obtained type depends from the value of this number:
that's hence a dependent type. (Remember the power type considered in Lecture 2 and compare the following type with it.)
Tuples
Fixpoint n_tuple (A:Type) (n: nat) :=
match n with
| 0 ⇒ A
| S n ⇒ ((n_tuple A n) × A)%type
end.
Check n_tuple.
Type -> nat -> Type
Fixpoint ints n : n_tuple nat n :=
match n with
| 0 ⇒ 0
| S n' ⇒ ((ints n'), n)
end.
Note that in the previous match the two branches have different
types :
- it is nat in the first case and
- it is (n_tuple nat n' × nat) in the second case.
Compute ints 99 : n_tuple nat 99.
a 100-uple
You already noticed that the type notations in Coq
can be very heavy espacially to handle type instanciation and all
the more when starting working with dependent types.
An essential feature of Coq to work smoothly with those types is
the ability to declare some arguments as being implicit and let Coq
infer the corresponding argument.
Some explanations about implicit arguments
Definition compose A B C (f: B → C) (g: A → B) : A → C :=
fun x ⇒ f (g x).
Check compose.
Print compose.
An option, is, at the time of the definition, to specify
which argument is implicit:
Definition compose_impl {A B C} (g: B → C) (f: A → B) : A → C :=
fun x ⇒ g (f x).
Another option is to automatically declare arguments
as implicit when they can be inferred as such:
Set Implicit Arguments.
Definition compose' A B C (f: B → C) (g: A → B) : A → C :=
fun x ⇒ f (g x).
Check compose'.
Print compose'.
Fail Definition thrice (A:Set)(f:A→A) := compose f (compose f f).
Definition thrice' (A:Set)(f:A→A) := compose' f (compose' f f).
Check thrice'.
Print thrice'.
Unset Implicit Arguments.
One can deactivate implicite arguments by using @:
Check compose_impl.
Check @compose_impl.
It can also happen that one need to explicitly provide explicit
arguments, for instance because of a partial application of arguments:
Check (compose' S).
Check (compose' (A := nat) S).
Inductive dependent types
Vectors
Fixpoint n_tuple (A:Type) (n: nat) :=
match n with
| 0 ⇒ A
| S n ⇒ ((n_tuple A n) × A)%type
end.
We shall now study another encoding of n-tuples.
We almost reuse the inductive definition of lists, but we add an
extra parameter representing the length of the list so that the
type itself is equiped with the information of the length of the list.
Inductive vect (A:Type) : nat → Type :=
| Vnil : vect A 0
| Vcons n : A → vect A n → vect A (S n).
Notice that this is a inductive type definition, but the type
being defined depends on values of type nat: it is a
inductive dependent type:
Check vect.
vect
: Type → nat → Type
This type vect is implemented in Coq standard library,
see file Vector.v.
One can declare the domain arguments of the constructors
(Vnil and Vcons) as being implicit arguments:
Arguments Vnil {A}.
Arguments Vcons {A}.
Let us see the encoding of triple (0,2,3) :
Check (Vcons 2 0 (Vcons 1 2 (Vcons 0 3 Vnil))).
The first argument of each constructor (numbers 2, 0, 1)
indicates the lengths of each sub-vector. Since this is
pretty predictable, and hence "boring", we could
even hide the n argument of Vcons. As follows:
Arguments Vcons {A} {n}.
Check (Vcons 0 (Vcons 2 (Vcons 3 Vnil))).
But this n argument is still there internally and
can be printed by requiring to print all information
(in CoqIDE, this is managed in the "View" panel):
Set Printing All.
Check (Vcons 0 (Vcons 2 (Vcons 3 Vnil))).
@Vcons nat 2 0 (@Vcons nat 1 2 (@Vcons nat 0 3 (@Vnil nat)))
: vect nat 3
Check (cons 0 (cons 2 (cons 3 nil))).
0 :: 2 :: 3 :: @nil nat)%list
Unset Printing All.
Conversion bewteen vectors and regular lists.
Require Import List.
Import ListNotations.
Fixpoint v2l {A} {n} (v : vect A n) : list A :=
match v with
| Vnil ⇒ []
| Vcons x v ⇒ x::(v2l v)
end.
But it is also possible to define the conversion in the other
direction:
Fixpoint l2v {A} (l: list A) : vect A (length l) :=
match l with
| [] ⇒ Vnil
| x :: l ⇒ Vcons x (l2v l)
end.
In the previous definition, we benefited greatly from
the implicit argument management offered by Coq. Try to define
the same conversion function without declaring the nat argument
as implicit...
The following definition (re-)computing the length of a vector
is actually useless since we already known this length, it is
the parameter n mentionned in the type vect.
Later, we could prove that
∀ A n (v:vect A n), length v = n.
Fixpoint length {A} {n} (v: vect A n) : nat :=
match v with
| Vnil ⇒ 0
| Vcons _ v ⇒ 1 + length v
end.
We could more simply define it as:
Definition length' {A} {n} (v: vect A n) : nat := n.
With such vectors, we avoid the usual issues encountered
when defining head and cons on lists, requiring to
use a default value or an option type. Indeed, we can specify
that we want to operate only on non-empty vectors, since they
have a Vect A (S n) type for some n.
On lists we can define the head function using a default value:
Definition head {A} (l:list A) (default : A) : A :=
match l with
| [] ⇒ default
| x :: _ ⇒ x
end.
Or returning in the option A type:
Definition head_opt {A} (l:list A) : option A :=
match l with
| [] ⇒ None
| x :: _ ⇒ Some x
end.
On vectors, we can more simply restict the
domain of the function Vhead:
Definition Vhead {A} {n} (v:vect A (S n)) : A :=
match v with
| Vcons x _ ⇒ x
end.
Notice that the match branch for Vnil just vanished,
and Coq did not complained about a missing case !
Actually, there does exist
such a case internally, filled by Coq with a somewhat arbitrary
code, that will never be accessed during computations.
Print Vhead.
Some check to see what is going on with the missing branch:
Print idProp.
Check idProp.
Print IDProp.
Compute Vhead ((Vcons 0 (Vcons 2 (Vcons 3 Vnil)))).
Fail Compute Vhead Vnil.
The error message is:
The term "Vnil" has type "vect ?A 0"
while it is expected to have type "vect ?A (S ?n)".
That is, there is a type mismatch.
Concatenating vectors
Print Nat.add.
0 + m = m
S p + m = S (p+m)
Fixpoint Vapp {A} {n m} (v:vect A n) (v':vect A m) : vect A (n+m) :=
match v with
| Vnil ⇒ v'
of type vect A m = vect A (0+m) = vect A (n+m) here
| Vcons x v ⇒ Vcons x (Vapp v v')
of type vect A (S (p+m)) = vect A ((S p)+m)
= vect A (n+m) here
end.
Alas, for other algorithms, we aren not so lucky. For instance,
consider the (naive) definition of the reverse of a vector.
A direct attempt is rejected as badly-typed. Coq would need
to know that vect A (n+1) is equal to vect A (1+n).
This is provably true, but not computationally true
(a.k.a "convertible" in the Coq jargon).
More precisely, we cannot have n+1 = 1+n by just the above
(Peano) equations, this would later need a proof by induction.
Fail Fixpoint Vrev {A} {n} (v:vect A n) : vect A n :=
match v with
| Vnil ⇒ Vnil
| Vcons x v ⇒ Vapp (Vrev v) (Vcons x Vnil)
end.
Which returns the following error message:
The term "Vapp (Vrev A n0 v0) (Vcons x Vnil)" has type
"vect A (n0 + 1)" while it is expected to have type
"vect A (S n0)".
We shall see two solutions to that problem:
A first solution is to use the Coq equality to "cast" between
vectors of equal lengths. We'll detail more later how Coq represents
its logical equality, for now let's just say that it's internally
a syntactic correspondence between the two side of the equation.
Hence "matching" over this proof h of type n=m would formally
"equate" n with m. And all it well.
Print eq.
Definition Vcast {A} {n} {m} (v: vect A n)(h : n = m) : vect A m :=
match h with
| eq_refl ⇒ v
end.
But for defining our Vrev, we need a proof of n+1=1+n.
Require Import Arith.
SearchPattern (_ + 1 = _).
That is a lemma called Nat.add_1_r, exactly what we need:
remember indeed that 1 + n reduces to S n
Fixpoint Vrev {A} {n} (v:vect A n) : vect A n :=
match v with
| Vnil ⇒ Vnil
| Vcons x v ⇒ Vcast (Vapp (Vrev v) (Vcons x Vnil)) (Nat.add_1_r _)
end.
Or, forcing the implicit argument to be treated explicitly:
Fixpoint Vrev' {A} {n} (v:vect A n) : vect A n :=
match v with
| Vnil ⇒ Vnil
| @Vcons _ m x v ⇒ Vcast (Vapp (Vrev' v) (Vcons x Vnil)) (Nat.add_1_r m)
end.
Issue: all computations with this Vrev is blocked, since
they cannot access Coq standard proof of Nat.add_1_r
(such a proof is said to be "opaque").
Compute Vrev (Vcons 1 (Vcons 2 (Vcons 3 Vnil))).
Solution inside the solution : do the proof ourself,
in a "transparent" way. Forget about this proof for the moment.
Lemma add_1_r n : n + 1 = S n.
Proof.
induction n; simpl; trivial. now f_equal.
Defined.
Print Nat.add_1_r.
Print add_1_r.
And not the usual Qed keyword, that would be opaque:
- Qed ends an opaque proof while
- Defined ends a transparent proof.
Fixpoint Vrev_transparent {A} {n} (v:vect A n) : vect A n :=
match v with
| Vnil ⇒ Vnil
| Vcons x v ⇒ Vcast (Vapp (Vrev_transparent v) (Vcons x Vnil)) (add_1_r _)
end.
Compute Vrev_transparent (Vcons 1 (Vcons 2 (Vcons 3 Vnil))).
Another solution : another definition of Vcast, way more complex,
that proceeds by induction over the vector instead of doing an
induction over the equality proof. You can skip the (ugly) details
of this function Vcast2, just notice that it does exist.
Definition Vcast2: ∀ {A m} (v: vect A m) {n}, m = n → vect A n.
Proof.
refine (fix cast {A m} (v: vect A m) {struct v} :=
match v in vect _ m' return ∀ n, m' = n → vect A n with
|Vnil ⇒ fun n ⇒ match n with
| 0 ⇒ fun H ⇒ Vnil
| S _ ⇒ fun H ⇒ False_rect _ _
end
|Vcons h w ⇒ fun n ⇒ match n with
| 0 ⇒ fun H ⇒ False_rect _ _
| S n' ⇒ fun H ⇒ Vcons h (cast w n' (f_equal pred H))
end
end); discriminate.
Defined.
Computing with this Vcast2 means deconstructing the whole vector
v before reconstructing everything (but with a length expressed
with the other side of the equality).
Print Vcast2.
fix ... match v ...Vil => ... Vnil ...
| Vcons n h w => ... Vcons h ...
To get the underlying algorithm behind all these details,
a nice way it to use the "extraction" tool, that convert a Coq
definition into some corresponding OCaml code, and drop on the
fly all logical parts (such as the equalities). More on that
to come later.
Require Extraction.
Extraction Vcast2.
And finally :
Fixpoint Vrev2 {A} {n} (v:vect A n) : vect A n :=
match v with
| Vnil ⇒ Vnil
| Vcons x v ⇒ Vcast2 (Vapp (Vrev2 v) (Vcons x Vnil)) (Nat.add_1_r _)
end.
And we can compute here, even with Coq standard proof of n+1=1+n.
Compute Vrev2 (Vcons 1 (Vcons 2 (Vcons 3 Vnil))).
As a conclusion:
- programming with dependent type may help a lot by ruling out some impossible cases and producing functions with rich types describing precisely the current situation.
- but it is not always convenient to proceed in this style, and may lead you to some definitional nightmare such as this Vrev.
Some more details about dependent types
Perfect binary trees, this time via inductive types.
Inductive fulltree (A:Type) : nat → Type :=
| FLeaf : A → fulltree A 0
| FNode n : fulltree A n → fulltree A n → fulltree A (S n).
Again, arguments of constructors may be treated implicitly:
Arguments FLeaf {A}.
Arguments FNode {A} {n}.
Check FNode (FNode (FLeaf 1) (FLeaf 2)) (FNode (FLeaf 1) (FLeaf 2)).
Dependent pairs (aka. dependent sums).
Print sigT.
Inductive sigT (A : Type) (P : A → Type) : Type :=
existT : ∀ x : A, P x → {x : A & P x}.
which is the inductive type, with syntax { ... & ... }
Check existT.
and its constructor
Just as the conclusion of a product may have a type B x which
depends on the value x of the input, here the right component
of this pair {x:A & B x} will have a type B x which depends
on the value x present on the left of this pair.
As the name existT of the constructor may suggests, this type
can also be seen as an existential type : "there exists an x in
A such that the right component is in B x".
Example : a type of all perfect binary trees on a domain A,
regardless of theirs depth
Definition all_fulltrees A := { n : nat & fulltree A n }.
We can exhibit an inhabitant of this sum type, that is
the pair of an m: nat and an inhabitant of fultree A m.
Definition some_fulltree : all_fulltrees nat :=
existT _ 1 (FNode (FLeaf 1) (FLeaf 2)).
the _ is here the "predicate" B, that Coq can infer here as being
fulltree nat.
Actually, Coq could even guess here the second argument (1),
by typing the last one (obtaining fulltree A 1 here).
Check existT _ _ (FNode (FLeaf 1) (FLeaf 2)).
To manipulate dependent sums,
we have some predefined projections:
Compute projT1 some_fulltree.
Compute projT2 some_fulltree.
of type : (fun n : nat => fulltree nat n) (projT1 some_fulltree)
which is convertible to : fulltree nat 1
Check projT1.
Check projT2.
Definition blist (A:Type) := list { n & fulltree A n }.
For instance :
Definition singleton {A} (a:A) : blist A := [ existT _ 0 (FLeaf a) ].
We will see later a few other existential types :
- the type of existential statements ∃ x:A, B x where A and B are in universe Prop.
- the "mixed" existential type {x:A | B x} (with underlying type name sig), where B is a logical statement in Prop, but A is in Type (we call A an "informative" or "relevent" type).
Fin: finite sets
Inductive Fin : nat → Type :=
| Zero n : Fin (S n)
| Succ n : Fin n → Fin (S n).
As usual, let us get rid of some "boring" arguments
Arguments Zero {n}.
Arguments Succ {n}.
Arguments Succ {n}.
Nobody can be in type Fin 0, since all constructors of Fin
have final types of the form Fin (S ...).
Then Fin 1 is a type with just one element
Check (Zero : Fin 1).
Fail Check (Succ Zero : Fin 1).
Then Fin 2 is a type with two elements, but no more
Check (Zero : Fin 2).
Check (Succ Zero : Fin 2).
Fail Check (Succ (Succ Zero) : Fin 2).
If we convert inhabitants of Fin n back to nat by forgetting
all the inner implicit arguments, then we indeed get all nat
numbers strictly less than n.
Fixpoint fin2nat {n} (m : Fin n) : nat :=
match m with
| Zero ⇒ 0
| Succ m' ⇒ S (fin2nat m')
end.
Definition all_fin3 : list (Fin 3) := [Zero; Succ Zero; Succ (Succ Zero)].
Compute List.map fin2nat all_fin3.
Note : another approach for defining such "bounded" integers is to
use an existential type to restrict nat.
Definition bounded_nat n := { p:nat | p < n }.
- Pros : easy projection to nat, no need for a reconstruction like fin2nat above.
- Cons : This implies to work with logical predicate < (in Prop, not the boolean comparison <? we have been using up to now) and requires to build arithmetical proofs. This is hence less suitable for the Vnth function below (no nice inductive structure).
Application: Vnth
Fixpoint Vnth {A} {n} (p:Fin n) : vect A n → A :=
match p with
| Zero ⇒ fun v ⇒ match v with Vcons x _ ⇒ x end
| Succ p ⇒ fun v ⇒ Vnth p (match v with Vcons _ v ⇒ v end)
end.
Notice that this type of programming is still relatively new in Coq,
and still very fragile. For instance, in the previous
example, one may be tempted to move the recursive call Vnth
inside the final match v, and hence write:
| Succ p => fun v => match v with Vcons _ v => Vnth p v end
But this is rejected by Coq for the moment. Similarly, no way
(yet ?) to move out the two fun v ⇒ and factorize them in one
fun v ⇒ outside of match p.
Example of use: with a vector of size 3, one may access to elements
at position 0, 1, 2 but not 3.
Definition testvec := Vcons 1 (Vcons 2 (Vcons 3 Vnil)).
Compute Vnth Zero testvec.
Compute Vnth (Succ Zero) testvec.
Compute Vnth (Succ (Succ Zero)) testvec.
Fail Compute Vnth (Succ (Succ (Succ Zero))) testvec.
This page has been generated by coqdoc