module Cat.Diagram.Product.Solver where
A solver for categories with binary productsπ
Much like the category solver, this module is split into two halves. The first implements an algorithm for normalizing expressions in the language of a category with binary products. The latter half consists of the usual reflection hacks required to transform Agda expressions into our internal expression type.
module NbE {o β} (π : Precategory o β) (cartesian : β A B β Product π A B) where
open Cat.Reasoning π
open Binary-products π cartesian
Expressionsπ
We begin by defining an expression type for a category with binary products. Mathematically, this /almost/ corresponds to the free category with binary products over a quiver, but we are working with un-quotiented syntax.
data βΆObβΆ : Type (o β β) where
_βΆββΆ_ : βΆObβΆ β βΆObβΆ β βΆObβΆ
_βΆ : Ob β βΆObβΆ
βΆ
_β§β : βΆObβΆ β Ob
β¦= β¦ X β§β ββ β¦ Y β§β
β¦ X βΆββΆ Y β§β = X
β¦ βΆ X βΆ β§β
data Expr : βΆObβΆ β βΆObβΆ β Type (o β β) where
: β {X} β Expr X X
βΆidβΆ _βΆββΆ_ : β {X Y Z} β Expr Y Z β Expr X Y β Expr X Z
: β {X Y} β Expr (X βΆββΆ Y) X
βΆΟββΆ : β {X Y} β Expr (X βΆββΆ Y) Y
βΆΟββΆ _,_β©βΆ : β {X Y Z} β Expr X Y β Expr X Z β Expr X (Y βΆββΆ Z)
βΆβ¨_βΆ : β {X Y} β Hom β¦ X β§β β¦ Y β§β β Expr X Y βΆ
Note that we also define a syntax for products of objects in this
free category, even though the ambient category π
already has binary products. The reason
for this is two-fold. The first, more mundane reason is that the unifier
will get very confused if we donβt do this. The second reason is much
more mathematically interesting, as it pertains to our choice of
normalization algorithm.
Much like the aforementioned category solver, we are going to be using a variant of Normalization By Evaluation (NbE for short). This class of normalization algorithms operates by constructing a domain of βvaluesβ, which are meant to denote the semantics of some expression. Normalization then occurs in 2 phases: an βevaluationβ phase where we transform expressions into values, and a βquotationβ phase where we reflect values back into expressions. As the values are meant to represent the semantics of an expression, each equivalence class of expressions ought to be mapped to the same value during evaluation. The quotation phase then plucks out a canonical representative for each one of these equivalence classes, which then becomes our normal form.
The particular variant of NbE that we are using is known as Typed NbE. What distinguishes it from Untyped NbE is the treatment of quotation. In Untyped NbE, quotation proceeds in a syntax-directed manner, which makes the enaction of Ξ·-laws1 more difficult. On the other hand, if we quote in a type directed manner, we can perform Ξ·-expansion at every possible opportunity, which simplifies the implementation considerably. This will result in larger normal forms, but the expressions the solver needs to deal with are small, so this isnβt a pressing issue.
Next, we define an interpretation of expressions back into morphisms. This will be used to state the all-important soundness theorem.
_β§β : β {X Y} β Expr X Y β Hom β¦ X β§β β¦ Y β§β
β¦= id
β¦ βΆidβΆ β§β = β¦ e1 β§β β β¦ e2 β§β
β¦ e1 βΆββΆ e2 β§β = Οβ
β¦ βΆΟββΆ β§β = Οβ
β¦ βΆΟββΆ β§β = β¨ β¦ e1 β§β , β¦ e2 β§β β©
β¦ βΆβ¨ e1 , e2 β©βΆ β§β = f β¦ βΆ f βΆ β§β
Valuesπ
Next, we define a type of Values. The goal here is to ensure
that we canβt have any eliminators (in our case, projections) applied to
introduction forms (in our case, β¨_,_β©
).
We also need to handle the normal associativity/identity equations, but
those will be handled by evaluating our expressions into presheaves.
data Value : βΆObβΆ β βΆObβΆ β Type (o β β) where
: β {X Y} β Hom β¦ X β§β β¦ Y β§β β Value X Y
vhom : β {X Y Z} β Value X Y β Value X Z β Value X (Y βΆββΆ Z) vpair
We now define our eliminators for values.
: β {X Y Z} β Value X (Y βΆββΆ Z) β Value X Y
vfst (vhom f) = vhom (Οβ β f)
vfst (vpair v1 v2) = v1
vfst
: β {X Y Z} β Value X (Y βΆββΆ Z) β Value X Z
vsnd (vhom f) = vhom (Οβ β f)
vsnd (vpair v1 v2) = v2
vsnd
: β {X} β Value X X
vid = vhom id vid
Quotationπ
As noted above, our quotation is type-directed to make applying
Ξ·-laws easier. When we encounter a v : Value X (Y βΆββΆ Z)
,
we will always Ξ·-expand it using the eliminators defined above. If
v
is a vpair
, then the
eliminators will compute away, and we will be left with the same value
we started with. If v
is a vhom
, then we will have Ξ·-expanded it, so
all of our normal forms will be /fully/ Ξ·-expanded.
As a terminological note, we call this function reflect
because quote
is a reserved keyword in Agda.
: β X Y β Value X Y β Hom β¦ X β§β β¦ Y β§β
reflect (Y βΆββΆ Z) v = β¨ (reflect X Y (vfst v)) , reflect X Z (vsnd v) β©
reflect X (vhom f) = f reflect X βΆ Y βΆ
Evaluationπ
Evaluation operates in much the same way as the category solver, where we evaluate to
Value X Y β Value X Z
instead of just
Value Y Z
. This allows us to apply the
associativity/identity equations, as well as the equation that
β¨ f , g β© β h β‘ β¨ f β h , g β h β©
.
: β {X Y Z} β Expr Y Z β Value X Y β Value X Z
eval = v
eval βΆidβΆ v (e1 βΆββΆ e2) v = eval e1 (eval e2 v)
eval = vfst v
eval βΆΟββΆ v = vsnd v
eval βΆΟββΆ v = vpair (eval e1 v) (eval e2 v)
eval βΆβ¨ e1 , e2 β©βΆ v = vhom (f β reflect _ _ v) eval βΆ f βΆ v
As noted earlier, we obtain normal forms by evaluating then quoting.
: β X Y β Expr X Y β Hom β¦ X β§β β¦ Y β§β
nf = reflect X Y (eval e vid) nf X Y e
Soundnessπ
Before proving soundness, we need to prove the normal battery of
random lemmas. The first states that quoting a vhom f
gives
us back f
.
: β X Y β (f : Hom β¦ X β§β β¦ Y β§β) β reflect X Y (vhom f) β‘ f
vhom-sound (Y βΆββΆ Z) f =
vhom-sound X (vhom (Οβ β f)) , reflect X Z (vhom (Οβ β f)) β© β‘β¨ apβ β¨_,_β© (vhom-sound X Y (Οβ β f)) (vhom-sound X Z (Οβ β f)) β©
β¨ reflect X Y
β¨ Οβ β f , Οβ β f β© β‘Λβ¨ β¨β©-unique refl refl β©
f β= refl vhom-sound X βΆ x βΆ f
Next, some soundless lemmas for our eliminators. We want to show that
applying each eliminator to a value corresponds to the correct thing
once interpreted into our category π
.
: β X Y Z β (v : Value X (Y βΆββΆ Z)) β reflect X Y (vfst v) β‘ Οβ β reflect X (Y βΆββΆ Z) v
vfst-sound (vhom f) =
vfst-sound X Y Z (vhom (Οβ β f)) β‘β¨ vhom-sound X Y (Οβ β f) β©
reflect X Y (Y βΆββΆ Z) f β©
Οβ β f β‘Λβ¨ reflβ©ββ¨ vhom-sound X (Y βΆββΆ Z) (vhom f) β
Οβ β reflect X (vpair v1 v2) =
vfst-sound X Y Z
reflect X Y v1 β‘Λβ¨ Οβββ¨β© β©(reflect X Y v1) , (reflect X Z v2) β© β
Οβ β β¨
: β X Y Z β (v : Value X (Y βΆββΆ Z)) β reflect X Z (vsnd v) β‘ Οβ β reflect X (Y βΆββΆ Z) v
vsnd-sound (vhom f) =
vsnd-sound X Y Z (vhom (Οβ β f)) β‘β¨ vhom-sound X Z (Οβ β f) β©
reflect X Z (Y βΆββΆ Z) f β©
Οβ β f β‘Λβ¨ reflβ©ββ¨ vhom-sound X (Y βΆββΆ Z) (vhom f) β
Οβ β reflect X (vpair v1 v2) =
vsnd-sound X Y Z
reflect X Z v2 β‘Λβ¨ Οβββ¨β© β©(reflect X Y v1) , (reflect X Z v2) β© β Οβ β β¨
We handle composition of values by interpreting expressions as functions /between/ values. So in a sense, this following lemma is a proof of soundness for our interpretation of composition.
: β X Y Z β (e : Expr Y Z) β (v : Value X Y)
sound-k β reflect X Z (eval e v) β‘ β¦ e β§β β reflect X Y v
= sym (idl _)
sound-k X Y Y βΆidβΆ v (e1 βΆββΆ e2) v =
sound-k X Y Z (eval e1 (eval e2 v)) β‘β¨ sound-k X _ Z e1 (eval e2 v) β©
reflect X Z _ (eval e2 v) β‘β¨ reflβ©ββ¨ sound-k X Y _ e2 v β©
β¦ e1 β§β β reflect X _ _ _ β©
β¦ e1 β§β β β¦ e2 β§β β reflect X Y v β‘β¨ assoc
β¦ e1 βΆββΆ e2 β§β β reflect X Y v β(Y βΆββΆ Z) Y βΆΟββΆ v = vfst-sound X Y Z v
sound-k X (Y βΆββΆ Z) Z βΆΟββΆ v = vsnd-sound X Y Z v
sound-k X (Z1 βΆββΆ Z2) βΆβ¨ e1 , e2 β©βΆ v =
sound-k X Y (eval e1 v) , reflect X Z2 (eval e2 v) β© β‘β¨ apβ β¨_,_β© (sound-k X Y Z1 e1 v) (sound-k X Y Z2 e2 v) β©
β¨ reflect X Z1 _ β©
β¨ β¦ e1 β§β β reflect X Y v , β¦ e2 β§β β reflect X Y v β© β‘Λβ¨ β¨β©β
β¨ β¦ e1 β§β , β¦ e2 β§β β© β reflect X Y v β= vhom-sound X Z _ sound-k X Y Z βΆ x βΆ v
The final soundness proof: normalizing an expression gives us the same morphism as naively interpreting the expression.
: β X Y β (e : Expr X Y) β nf X Y e β‘ β¦ e β§β
sound = sound-k X X Y e vid β elimr (vhom-sound X X id) sound X Y e
Solver interfaceπ
In order to make the reflection easier later, we bundle up the soundness proof. Marking this as abstract is very important. This prevents agda from unfolding into an absolutely enormous proof when used as a macro, which is critical for performance.
abstract
: β X Y β (e1 e2 : Expr X Y) β nf X Y e1 β‘ nf X Y e2 β β¦ e1 β§β β‘ β¦ e2 β§β
solve = sym (sound X Y e1) ββ p ββ sound X Y e2 solve X Y e1 e2 p
Reflectionπ
As per usual, this is the hard part. Reflection is normally quite tricky, but the situation here is even harder than the category solver, as we need to reflect on objects as well as morphisms.
We begin by defining a bunch of pattern synonyms for matching on various fields of precategories, as well as objects + morphisms that arise from the product structure.
The situation here is extremely fiddly when it comes to implicit
arguments, as we not only need to get the number correct, but also their
multiplicity. Record projections always mark the records parameters as
hidden
and quantity-0
, so we need to take care to do
the same in these patterns.
module Reflection where
private
pattern is-product-field X Y args =
_ hmβ· _ hmβ· _ hmβ· -- category args
-- objects of product
X hmβ· Y hmβ· _ hmβ· -- apex
_ hmβ· _ hmβ· -- projections
_ vβ· -- is-product record argument
argspattern product-field X Y args =
_ hmβ· _ hmβ· _ hmβ· -- category args
-- objects of product
X hmβ· Y hmβ· _ vβ· -- product record argument
argspattern category-field args = _ hmβ· _ hmβ· _ vβ· args
pattern βββ X Y =
(quote Product.apex) (product-field X Y [])
def pattern βidβ X =
(quote Precategory.id) (category-field (X hβ· []))
def pattern βββ X Y Z f g =
(quote Precategory._β_) (category-field (X hβ· Y hβ· Z hβ· f vβ· g vβ· []))
def pattern βΟββ X Y =
(quote (Product.Οβ)) (product-field X Y [])
def pattern βΟββ X Y =
(quote (Product.Οβ)) (product-field X Y [])
def pattern ββ¨β©β X Y Z f g =
(quote (is-product.β¨_,_β©)) (is-product-field Y Z (X hβ· f vβ· g vβ· [])) def
Next, we define some helpers to make constructing things in the NbE
module easier.
: Name β List (Arg Term) β Term
mk-nbe-con =
mk-nbe-con con-name args (unknown hβ· unknown hβ· unknown hβ· unknown hβ· args)
con con-name
: Term β Term β List (Arg Term) β List (Arg Term)
mk-nbe-call = unknown hβ· unknown hβ· cat vβ· cart vβ· args mk-nbe-call cat cart args
We also define some helpers for building quoted calls to NbE.nf
and NbE.solve
.
: Term β Term β Term β Term β Term β Term
βnfβ =
βnfβ cat cart x y e (quote NbE.nf) (mk-nbe-call cat cart (x vβ· y vβ· e vβ· []))
def
: Term β Term β Term β Term β Term β Term β Term
βsolveβ =
βsolveβ cat cart x y lhs rhs (quote NbE.solve) $
def (x vβ· y vβ· lhs vβ· rhs vβ· βreflβ vβ· []) mk-nbe-call cat cart
Now for the meat of the reflection. build-obj-expr
will
construct quoted terms of type NbE.βΆObβΆ
from
quoted terms of type Precategory.Ob
.
build-hom-expr
does the same thing, but for NbE.Expr
and
Precategory.Hom
.
Note that we apply all implicits to constructors in
build-hom-expr
. If we donβt do this, Agda will get
very upset.
: Term β Term
build-obj-expr (βββ X Y) =
build-obj-expr (quote NbE.βΆObβΆ._βΆββΆ_) (build-obj-expr X vβ· build-obj-expr Y vβ· [])
con =
build-obj-expr X (quote NbE.βΆObβΆ.βΆ_βΆ) (X vβ· [])
con
: Term β Term
build-hom-expr (βidβ X) =
build-hom-expr (quote NbE.Expr.βΆidβΆ) $
mk-nbe-con
build-obj-expr X hβ· [](βββ X Y Z f g) =
build-hom-expr (quote NbE.Expr._βΆββΆ_) $
mk-nbe-con
build-obj-expr X hβ· build-obj-expr Y hβ· build-obj-expr Z hβ·
build-hom-expr f vβ· build-hom-expr g vβ· [](βΟββ X Y) =
build-hom-expr (quote NbE.Expr.βΆΟββΆ) $
mk-nbe-con
build-obj-expr X hβ· build-obj-expr Y hβ· [](βΟββ X Y) =
build-hom-expr (quote NbE.Expr.βΆΟββΆ) $
mk-nbe-con
build-obj-expr X hβ· build-obj-expr Y hβ· [](ββ¨β©β X Y Z f g) =
build-hom-expr (quote NbE.Expr.βΆβ¨_,_β©βΆ) $
mk-nbe-con
build-obj-expr X hβ· build-obj-expr Y hβ· build-obj-expr Z hβ·
build-hom-expr f vβ· build-hom-expr g vβ· []=
build-hom-expr f (quote NbE.Expr.βΆ_βΆ) (f vβ· []) con
Now, for the solver interface. This follows the usual pattern: we
create a list of names that we will pass to withReduceDefs
, which will prevent Agda
from normalizing away the things we want to reflect upon.
: List Name
dont-reduce =
dont-reduce quote Precategory.Hom β·
quote Precategory.id β·
quote Precategory._β_ β·
quote Product.apex β·
quote Product.Οβ β·
quote Product.Οβ β·
quote is-product.β¨_,_β© β· []
We will need to recover the objects from some quoted hom to make the calls to the solver/normaliser.
: Term β TC (Term Γ Term)
get-objects = ((infer-type tm >>= normalise) >>= wait-just-a-bit) >>= Ξ» where
get-objects tm (def (quote Precategory.Hom) (category-field (x vβ· y vβ· []))) β
(x , y)
pure β
tp "Can't determine objects: " β· termErr tp β· [] typeError $ strErr
We also make some debugging macros, which are very useful for when you want to examine the exact quoted representations of objects/homs.
: β {o β} (π : Precategory o β) (cartesian : β X Y β Product π X Y) β Term β Term β TC β€
obj-repr-macro =
obj-repr-macro cat cart hom hole
withReconstructed true $
withNormalisation false $(false , dont-reduce) $ do
withReduceDefs (x , y) β get-objects hom
βxβ β build-obj-expr <$> normalise x
βyβ β build-obj-expr <$> normalise y"Determined objects of " β· termErr hom β· strErr " to be\n " β·
typeError $ strErr "\nAnd\n " β·
termErr x β· strErr "\nWith quoted representations\n " β·
termErr y β· strErr "\nAnd\n " β·
termErr βxβ β· strErr
termErr βyβ β· []
: β {o β} (π : Precategory o β) (cartesian : β X Y β Product π X Y) β Term β Term β TC β€
hom-repr-macro =
hom-repr-macro cat cart hom hole
withReconstructed true $
withNormalisation false $(false , dont-reduce) $ do
withReduceDefs (x , y) β get-objects hom
βxβ β build-obj-expr <$> normalise x
βyβ β build-obj-expr <$> normalise y
βhomβ β build-hom-expr <$> normalise hom"The morphism\n " β·
typeError $ strErr "\nis represented by\n " β·
termErr hom β· strErr "\nwith objects\n " β·
termErr βhomβ β· strErr "\nAnd\n " β·
termErr βxβ β· strErr termErr βyβ β· []
Now, the simplifier and solver reflection. This just puts together all of our bits from before.
There is one subtlety here with regards to
withReconstructed
. We are reflecting on the record
parameters to Product
and is-product
to determine the objects
involved in things like β¨_,_β©
,
which Agda will mark as unknown
by default. This will cause
build-obj-expr
to then fail when we
have expressions involving nested _β_
.
Wrapping everything in withReconstructed
causes Agda to
fill in these arguments with their actual values, which then fixes the
issue.
: β {o β} (π : Precategory o β) (cartesian : β X Y β Product π X Y) β Term β Term β TC β€
simpl-macro =
simpl-macro cat cart hom hole
withReconstructed true $
withNormalisation false $(false , dont-reduce) $ do
withReduceDefs (x , y) β get-objects hom
βxβ β build-obj-expr <$> normalise x
βyβ β build-obj-expr <$> normalise y
βhomβ β build-hom-expr <$> normalise hom
βcatβ β quoteTC cat
βcartβ β quoteTC cart(βnfβ βcatβ βcartβ βxβ βyβ βhomβ) unify hole
Finally, we define the user-facing interface as a series of macros.
macro
: β {o β}
products-obj-repr! β (π : Precategory o β) (cartesian : β X Y β Product π X Y)
β Term β Term β TC β€
= Reflection.obj-repr-macro
products-obj-repr!
: β {o β}
products-repr! β (π : Precategory o β) (cartesian : β X Y β Product π X Y)
β Term β Term β TC β€
= Reflection.hom-repr-macro
products-repr!
: β {o β}
products-simpl! β (π : Precategory o β) (cartesian : β X Y β Product π X Y)
β Term β Term β TC β€
= Reflection.simpl-macro products-simpl!
module _ {o β} {C : Precategory o β} (cart : β X Y β Product C X Y) {x y : β C β} {h1 h2 : C .Precategory.Hom x y} where
open Reflection
private
: Term β TC β€
products-worker = withReconstructed true $ withNormalisation true $ withReduceDefs (false , dont-reduce) do
products-worker goal
`h1 β wait-for-type =<< quoteTC h1
`h2 β quoteTC h2
`x β quoteTC x
`y β quoteTC y
βcartβ β quoteTC cart
let
= build-obj-expr `x
βxβ = build-obj-expr `y
βyβ = build-hom-expr `h1
βlhsβ = build-hom-expr `h2
βrhsβ
(Reflection.βsolveβ unknown βcartβ βxβ βyβ βlhsβ βrhsβ) <|> do
unify goal
βcatβ β quoteTC C(βnfβ βcatβ βcartβ βxβ βyβ βlhsβ)
vlhs β normalise (βnfβ βcatβ βcartβ βxβ βyβ βrhsβ)
vrhs β normalise
typeError"Could not equate the following expressions:\n "
[ "\nAnd\n " , termErr `h2
, termErr `h1 , "\nReflected expressions\n "
, "\nAnd\n " , termErr βrhsβ
, termErr βlhsβ , "\nComputed normal forms\n "
, strErr "\nAnd\n " , termErr vrhs
, termErr vlhs , strErr
]
: {@(tactic products-worker) p : h1 β‘ h2} β h1 β‘ h2
products-wrapper {p = p} = p
products-wrapper
macro
: Term β TC β€
products! = flip unify (def (quote products-wrapper) []) products!
Demoπ
Wow, that was a lot of hard work! Letβs marvel at the fruits of our labor.
private module Tests {o β} (π : Precategory o β) (cartesian : β X Y β Product π X Y) where
open Precategory π
open Binary-products π cartesian
open NbE π cartesian
: β {X Y Z} β (f : Hom X (Y ββ Z))
test-Ξ· β f β‘ β¨ Οβ β f , Οβ β f β©
= products! cartesian
test-Ξ· f
: β {X Y Z} β (f : Hom X Y) β (g : Hom X Z)
test-Ξ²β β Οβ β β¨ f , g β© β‘ f
= products! cartesian
test-Ξ²β f g
: β {X Y Z} β (f : Hom X Y) β (g : Hom X Z)
test-Ξ²β β Οβ β β¨ f , g β© β‘ g
= products! cartesian
test-Ξ²β f g
: β {W X Y Z} β (f : Hom X Y) β (g : Hom X Z) β (h : Hom W X)
test-β¨β©β β β¨ f β h , g β h β© β‘ β¨ f , g β© β h
= products! cartesian
test-β¨β©β f g h
-- If you don't have 'withReconstructed' on, this test will fail!
: β {W X Y Z} β (f : Hom W X) β (g : Hom W Y) β (h : Hom W Z)
test-nested β β¨ β¨ f , g β© , h β© β‘ β¨ β¨ f , g β© , h β©
{W} {X} {Y} {Z} f g h = products! cartesian
test-nested
: β {W X Y Z} β (f : Hom (W ββ X) (W ββ Y)) β (g : Hom (W ββ X) Z)
test-big β (Οβ β β¨ f , g β©) β id β‘ id β β¨ Οβ , Οβ β© β f
= products! cartesian test-big f g
In our context, an Ξ·-law is something like
β¨ Οβ β f , Οβ β f β© β‘ f
, where we have an introduction form wrapping a bunch of eliminators applied to the same expression.β©οΈ