The way to use type classes is to think of them as additional structure on original structure, and not a larger structure that contains the original one.
Let's explain this with a simple example first. A pointed type is given by a carrier type A
and an element a : A
, called the point. We can formalize this in two ways.
The first one is display map:
Structure Pointed : Type := {
carrier :> Type ;
point : carrier
}.
The second one is a fibration:
Structure Pointed (A : Type) := {
point : A
}.
(Note: the terimonology comes from category theory. In the first way we think of the structure Pointed
displayed over Type
by the projection carrier
. In the second way we think of Pointed
as a fibration that assigns to each type A
the type of structures Pointed A
.)
Whether one should use display maps or fibrations in a given situation depends on the situation. Type classes work well with fibrations, while display maps work better with coercions and canonical structures.
So in your case we should be using the fibered approach: a category with finite products is a category C
together with a fibration FinProd : C β Type
that assings to each C
the type of all finite-product structures on C
, and similarly for other structures of interest.
This leads to the following formalization style:
(* All definitions are incomplete, but they convey the message. *)
(* The structure of categories. *)
Structure Category : Type := {
obj :> Type ;
hom : obj -> obj -> Type
}.
Arguments hom {_} _ _.
(* The terminal object on a category. *)
Class Terminal (C : Category) := {
one :> C ;
}.
(* The type class of binary-product-structures on a given category. *)
Class BinProd (C : Category) : Type := {
prod : C -> C -> C
}.
(* Finite products structure on a category. *)
Class FinProd (C : Category) : Type := {
list_prod : list C -> C
}.
(* Conversion from finite-product structure to binary structure. *)
(* Note the backquote mechanism, which instructs the instance *)
(* mechanism that the argument can be filled in automatically. *)
Instance BinProd_FinProd {C : Category} {FP : FinProd C} : BinProd C :=
{| prod := fun x y : C => list_prod (x :: y :: nil) |}.
(* Conversion from finite-product structure to terminal object. *)
Instance Terminal_FinProd {C : Category} {FP : FinProd C} : Terminal C :=
{| one := list_prod nil |}.
(* To test how things work, let's also imagine a third structure
called "conjunction system" that is formalized in the "display map"
style. *)
Class ConjunctionSystem (C : Category) := {
carrier : Type ;
conj : carrier -> carrier -> carrier
}.
(* Every binary product category can be converted to a conjunction system. *)
Instance ConjunctionSystem_BinProd {C : Category} {BP : BinProd C} : ConjunctionSystem C :=
{| carrier := C ; conj := prod |}.
Section Example.
(* Suppose we have a category C. *)
Variable (C : Category).
(* Suppose C has a finite product structure. *)
Variable (FP : FinProd C).
(* Then the category has a terminal object and binary products.
We can simply mentioned them. *)
Lemma cow : forall a, prod a one = one.
Proof.
intro a.
(* let us verify that Coq deduced prod and one from FP. *)
unfold one, prod, BinProd_FinProd, Terminal_FinProd.
(* We cannot finish this proof, it's only a demo. *)
Admitted.
(* Conjunction system on C is derived automatically, so we can
mention conj wherever we like. *)
Definition dog x := conj x x.
(* The tactic typeclasses eauto finds instances for us. *)
Definition parrot : ConjunctionSystem C.
Proof.
typeclasses eauto.
Defined.
(* Of course, we can also use them explicitly. Note that we
do not have to provide any arguments, they are derived. *)
Definition eagle : ConjunctionSystem C := ConjunctionSystem_BinProd.
End Example.
The instances tell Coq things like "this is how you get BindProd C
from FinProd C
, and this is how you get Terminal C
from FinProd C
". What the instances cannot do is "if you need structure CowStomach
and you have a structure Cow
which contains CowStomach
as a field, just project the field" (that would be a display map approach).