module Cat.Instances.Free where
Free categoriesπ
Given a graph we construct a strict category in the following manner:
- The objects of are the vertices of
- The morphisms in are given by finite paths in Finite paths are defined by the following indexed-inductive type
data Path-in : G.Vertex β G.Vertex β Type (o β β) where nil : β {a} β Path-in a a cons : β {a b c} β G.Edge a b β Path-in b c β Path-in a c
That is: a path is either empty, in which case it starts and ends at itself (these are the identity morphisms), or we can form a path from by starting with a path and precomposing with an edge Much of the code below is dedicated to characterising the identity types between paths. Indeed, to construct a category we must show that paths in form a set.
We have a couple of options here:
We can construct paths by recursion, on their length: Defining a βpath from to of length β by recursion on and then defining a path from to as being a pair where and is a path of length
We can define paths by induction, as is done above.
The former approach makes it easy to show that paths form a set: we can directly construct the set of paths, by recursion, then project the underlying type if necessary. But working with these paths is very inconvenient, since we have to deal with explicit identities between the endpoints. The latter approach makes defining functions on paths easy, but showing that they are a set is fairly involved. Letβs see how to do it.
The first thing weβll need is a predicate expressing that a path really encodes the empty path, and we have an identity of vertices We can do this by recursion: If is nil, then we can take this to be the unit type, otherwise itβs the bottom type.
is-nil : β {a b} β Path-in a b β Type (o β β) is-nil nil = Lift _ β€ is-nil (cons _ _) = Lift _ β₯
Weβd like to define a relation standing for an identification of paths But observe what happens in the case where weβve built up a path by adding an edge: We know that the edges start at and the inner paths end at but the inner vertex may vary!
Weβll need to package an identification
in the relation for
and so, weβll have to encode for a path
over some identification of their start points. Thatβs why we
have path-codep
and not βpath-codeβ.
A value in
codes for a path
over
path-codep : β (a : I β G.Vertex) {c} β Path-in (a i0) c β Path-in (a i1) c β Type (o β β)
Note that in the case where
Agda refines
to be definitionally
and we can no longer match on the right-hand-side path
Thatβs where the is-nil
predicate comes in: We
say that
is equal to
if is-nil
holds. Of course, a cons
and a nil
can never be
equal.
path-codep a nil ys = is-nil ys path-codep a (cons x xs) nil = Lift _ β₯
The recursive case constructs an identification of cons
cells as a
triple consisting of an identification between their intermediate
vertices, and over that data, an identification between the added edges,
and a code for an identification between the tails.
path-codep a {c} (cons {b = b} x xs) (cons {b = b'} y ys) = Ξ£[ bs β (b β‘ b') ] (PathP (Ξ» i β G.Edge (a i) (bs i)) x y Γ path-codep (Ξ» i β bs i) xs ys)
By recursion on the paths and the code for an equality, we can show
that if we have a code for an identification, we can indeed compute an
identification. The most involved case is actually when the lists are
empty, in which case we must show that is-nil(xs)
1
implies that
but it must be over an arbitrary identification
2. Fortunately, vertices in a graph
live in a set, so
is reflexivity.
path-encode : β (a : I β G.Vertex) {c} xs ys β path-codep a xs ys β PathP (Ξ» i β Path-in (a i) c) xs ys path-encode a (cons x xs) (cons y ys) (p , q , r) i = cons {a = a i} {b = p i} (q i) $ path-encode (Ξ» i β p i) xs ys r i path-encode a nil ys p = lemma (Ξ» i β a (~ i)) ys p where lemma : β {a b} (p : a β‘ b) (q : Path-in a b) β is-nil q β PathP (Ξ» i β Path-in (p (~ i)) b) nil q lemma {a = a} p nil (lift lower) = to-pathp $ is-setβsubst-refl (Ξ» e β Path-in e a) G.Vertex-is-set (sym p) nil lemma _ (cons x p) ()
The next step is to show that codes for identifications between paths live in a proposition; But this is immediate by their construction: in every case, we can show that they are either literally a proposition (the base case) or built out of propositions: this last case is inductive.
path-codep-is-prop : β (a : I β G.Vertex) {b} β (p : Path-in (a i0) b) (q : Path-in (a i1) b) β is-prop (path-codep a p q) path-codep-is-prop a nil xs x y = is-nil-is-prop xs x y where is-nil-is-prop : β {a b} (xs : Path-in a b) β is-prop (is-nil xs) is-nil-is-prop nil x y = refl path-codep-is-prop a (cons h t) (cons h' t') (p , q , r) (p' , q' , r') = Ξ£-pathp (G.Vertex-is-set _ _ p p') $ Ξ£-pathp (is-propβpathp (Ξ» i β PathP-is-hlevel' 1 G.Edge-is-set _ _) q q') (is-propβpathp (Ξ» i β path-codep-is-prop (Ξ» j β G.Vertex-is-set _ _ p p' i j) t t') r r')
And finally, by proving that there is a code for the reflexivity path, we can show that we have an identity system in the type of paths from to given by their codes. Since these codes are propositions, and identity systems give a characterisation of a typeβs identity types, we conclude that paths between a pair of vertices live in a set!
path-codep-refl : β {a b} (p : Path-in a b) β path-codep (Ξ» i β a) p p path-codep-refl nil = lift tt path-codep-refl (cons x p) = refl , refl , path-codep-refl p path-identity-system : β {a b} β is-identity-system {A = Path-in a b} (path-codep (Ξ» i β a)) path-codep-refl path-identity-system = set-identity-system (path-codep-is-prop Ξ» i β _) (path-encode _ _ _) path-is-set : β {a b} β is-set (Path-in a b) path-is-set {a = a} = identity-systemβhlevel 1 path-identity-system $ path-codep-is-prop Ξ» i β a
path-decode : β {a b} {xs ys : Path-in a b} β xs β‘ ys β path-codep (Ξ» _ β a) xs ys path-decode = Equiv.from (identity-system-gives-path path-identity-system)
The path categoryπ
By comparison, constructing the actual precategory of paths is almost trivial. The composition operation, concatenation, is defined by recursion over the left-hand-side path. This is definitionally unital on the left.
_++_ : β {a b c} β Path-in G a b β Path-in G b c β Path-in G a c nil ++ ys = ys cons x xs ++ ys = cons x (xs ++ ys)
Right unit and associativity are proven by induction.
++-idr : β {a b} (xs : Path-in G a b) β xs ++ nil β‘ xs ++-idr nil = refl ++-idr (cons x xs) = ap (cons x) (++-idr xs) ++-assoc : β {a b c d} (p : Path-in G a b) (q : Path-in G b c) (r : Path-in G c d) β (p ++ q) ++ r β‘ p ++ (q ++ r) ++-assoc nil q r = refl ++-assoc (cons x p) q r = ap (cons x) (++-assoc p q r)
And thatβs it! Note that we must compose paths backwards, since the type of the concatenation operation and the type of morphism composition are mismatched (theyβre reversed).
open Precategory Path-category : Precategory o (o β β) Path-category .Ob = G.Vertex Path-category .Hom = Path-in G Path-category .Hom-set _ _ = path-is-set G Path-category .id = nil Path-category ._β_ xs ys = ys ++ xs Path-category .idr f = refl Path-category .idl f = ++-idr f Path-category .assoc f g h = ++-assoc h g f
Moreover, free categories are always gaunt: they are automatically strict and,
as can be seen with a bit of work, univalent. Univalence follows because
any non-trivial isomorphism would have to arise as a cons
, but cons
can never be
nil
β which would
be required for a composition to equal the identity.
While types prevent us from directly stating βif a map is invertible,
it is nil
β, we can
nevertheless pass around some equalities to make this induction
acceptable.
Path-category-is-category : is-category Path-category Path-category-is-category = r where module Pc = Cat.Reasoning Path-category remβ : β {x y} (j : Pc.Isomorphism x y) β Ξ£ (x β‘ y) Ξ» p β PathP (Ξ» i β Pc.Isomorphism x (p i)) Pc.id-iso j remβ {x = x} im = go im (im .Pc.to) refl (path-decode G (im .Pc.invr)) where go : β {y} (im : Pc.Isomorphism x y) (j' : Path-in G x y) β j' β‘ im .Pc.to β path-codep G (Ξ» _ β x) (j' ++ im .Pc.from) nil β Ξ£ (x β‘ y) Ξ» p β PathP (Ξ» i β Pc.Isomorphism x (p i)) Pc.id-iso im go im nil p q = refl , ext p r : is-category Path-category r .to-path i = remβ i .fst r .to-path-over i = remβ i .snd Path-category-is-gaunt : is-gaunt Path-category Path-category-is-gaunt = record { has-category = Path-category-is-category ; has-strict = hlevel 2 }
As a free constructionπ
We shall now prove that free categories are, indeed, free. Explicitly, we shall show that they are free objects relative to the underlying graph functor.
Let be a graph, a strict category, and a graph homomorphism between and the underying graph of We can extend to a function from paths in to morphisms in via induction.
module _ (C : Ξ£ (Precategory o β) is-strict) where private module C = Cat.Reasoning (C .fst) β£Cβ£ : Graph o β β£Cβ£ = Strict-catsβͺGraphs .Fβ C path-fold : (f : Graph-hom G β£Cβ£) β β {x y} β Path-in G x y β C.Hom (f .vertex x) (f .vertex y) path-fold f nil = C.id path-fold f (cons e p) = path-fold f p C.β f .edge e
Moreover,
is functorial; it definitionally takes the nil
to id
, and we can easily show that it
preserves composites with some easy induction.
path-fold-++ : {f : Graph-hom G β£Cβ£} β β {x y z} (p : Path-in G x y) (q : Path-in G y z) β path-fold f (p ++ q) β‘ path-fold f q C.β path-fold f p path-fold-++ nil q = sym (C.idr _) path-fold-++ (cons e p) q = C.pushl (path-fold-++ p q) PathF : Graph-hom G β£Cβ£ β Functor (Path-category G) (C .fst) PathF f .Fβ = f .vertex PathF f .Fβ = path-fold f PathF f .F-id = refl PathF f .F-β p q = path-fold-++ q p
path-fold-unique : β {f : Graph-hom G β£Cβ£} β (h : β {x y} β Path-in G x y β C.Hom (f .vertex x) (f .vertex y)) β (β {x} β h (nil {a = x}) β‘ C.id) β (β {x y z} (e : G .Edge x y) (p : Path-in G y z) β h (cons e p) β‘ (h p C.β f .edge e)) β β {x y} (p : Path-in G x y) β h p β‘ path-fold f p path-fold-unique h n c nil = n path-fold-unique h n c (cons e p) = c e p β apβ C._β_ (path-fold-unique h n c p) refl path-reduce : β {x y} β Path-in (Strict-catsβͺGraphs .Fβ C) x y β C.Hom x y path-reduce = path-fold Graphs.id
We can also give a nice characterisation paths between functors out of path categories. In particular, we only need to check that two functors agree on edges; functoriality takes care of the rest.
Path-category-functor-path : {F F' : Functor (Path-category G) C} β (p0 : β x β F .Fβ x β‘ F' .Fβ x) β (p1 : β {x y} (e : G .Edge x y) β PathP (Ξ» i β C.Hom (p0 x i) (p0 y i)) (F .Fβ (cons e nil)) (F' .Fβ (cons e nil))) β F β‘ F'
The proof involves some cubical yoga, so we will hide it from the innocent reader.
Path-category-functor-path {G = G} {F} {F'} p0 p1 = Functor-path p0 p1-paths where p1-paths : β {x y} β (p : Path-in G x y) β PathP (Ξ» i β C.Hom (p0 x i) (p0 y i)) (F .Fβ p) (F' .Fβ p) p1-paths {x = x} nil i = hcomp (β i) Ξ» where j (i = i0) β F .F-id (~ j) j (i = i1) β F' .F-id (~ j) j (j = i0) β C.id p1-paths (cons e p) i = hcomp (β i) Ξ» where j (i = i0) β F .F-β p (cons e nil) (~ j) j (i = i1) β F' .F-β p (cons e nil) (~ j) j (j = i0) β p1-paths p i C.β p1 e i
With these lemmas out of the way, we can return to our orginal goal.
The unit of the free object is given by the graph homomorphism that
takes an edge to a singleton path, the universal morphism is given by
our functor PathF
from earlier, and the
universal property follow directly from our hypotheses.
Free-category : β (G : Graph β β) β Free-object Strict-catsβͺGraphs G Free-category G .Free-object.free = Path-category G , hlevel 2 Free-category G .Free-object.unit .vertex v = v Free-category G .Free-object.unit .edge e = cons e nil Free-category G .Free-object.fold {C} = PathF C Free-category G .Free-object.commute {Y = C} {f = f} = Graph-hom-path (Ξ» _ β refl) (Ξ» _ β idl _) where open Precategory (C .fst) Free-category G .Free-object.unique {Y = C} {f} F p = Path-category-functor-path (Ξ» x i β p i .vertex x) (Ξ» e β to-pathp (from-pathp (Ξ» i β p i .edge e) β sym (idl _))) where open Precategory (C .fst)
Free-categories : Functor (Graphs β β) (Strict-cats β β) Free-categories = free-objectsβfunctor Free-category Free-categoriesβ£Underlying-graph : Free-categories {β} β£ Strict-catsβͺGraphs Free-categoriesβ£Underlying-graph = free-objectsβleft-adjoint Free-category
-- Defined by hand to be more universe polymorphic. path-map : β {x y} β (f : Graph-hom G H) β Path-in G x y β Path-in H (f # x) (f # y) path-map f nil = nil path-map f (cons e p) = cons (f .edge e) (path-map f p) path-map-id : β {x y} β (p : Path-in G x y) β path-map Graphs.id p β‘ p path-map-id nil = refl path-map-id (cons e p) = ap (cons e) (path-map-id p) path-map-β : β {x y} β {f : Graph-hom H K} {g : Graph-hom G H} β (p : Path-in G x y) β path-map (f Graphs.β g) p β‘ path-map f (path-map g p) path-map-β nil = refl path-map-β (cons e p) = apβ cons refl (path-map-β p) module _ {C D : Ξ£ (Precategory o β) is-strict} (F : Functor (C .fst) (D .fst)) where private module C = Cat.Reasoning (C .fst) module D = Cat.Reasoning (D .fst) module F = Functor F path-reduce-natural : β {x y} β (p : Path-in (Strict-catsβͺGraphs .Fβ C) x y) β path-reduce D (path-map (Strict-catsβͺGraphs .Fβ F) p) β‘ F.β (path-reduce C p) path-reduce-natural nil = sym F.F-id path-reduce-natural (cons e p) = apβ D._β_ (path-reduce-natural p) refl β sym (F.F-β _ _) module _ {C : Ξ£ (Precategory o β) is-strict} where private module C = Cat.Reasoning (C .fst) path-reduce-map : β {x y} β {f : Graph-hom G (Strict-catsβͺGraphs .Fβ C)} β (p : Path-in G x y) β path-reduce C (path-map f p) β‘ path-fold C f p path-reduce-map nil = refl path-reduce-map (cons e p) = apβ C._β_ (path-reduce-map p) refl