module Cat.Diagram.Product.Solver whereA 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 π cartesianExpressionsπ
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
βΆidβΆ : β {X} β Expr X X
_βΆββΆ_ : β {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 YNote 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 βΆ β§β = fValuesπ
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
vhom : β {X Y} β Hom β¦ X β§β β¦ Y β§β β Value X Y
vpair : β {X Y Z} β Value X Y β Value X Z β Value X (Y βΆββΆ Z)We now define our eliminators for values.
vfst : β {X Y Z} β Value X (Y βΆββΆ Z) β Value X Y
vfst (vhom f) = vhom (Οβ β f)
vfst (vpair v1 v2) = v1
vsnd : β {X Y Z} β Value X (Y βΆββΆ Z) β Value X Z
vsnd (vhom f) = vhom (Οβ β f)
vsnd (vpair v1 v2) = v2
vid : β {X} β Value X X
vid = vhom idQuotationπ
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.
reflect : β X Y β Value X Y β Hom β¦ X β§β β¦ Y β§β
reflect X (Y βΆββΆ Z) v = β¨ (reflect X Y (vfst v)) , reflect X Z (vsnd v) β©
reflect X βΆ Y βΆ (vhom f) = fEvaluationπ
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 β©.
eval : β {X Y Z} β Expr Y Z β Value X Y β Value X Z
eval βΆidβΆ v = v
eval (e1 βΆββΆ e2) v = eval e1 (eval e2 v)
eval βΆΟββΆ v = vfst v
eval βΆΟββΆ v = vsnd v
eval βΆβ¨ e1 , e2 β©βΆ v = vpair (eval e1 v) (eval e2 v)
eval βΆ f βΆ v = vhom (f β reflect _ _ v)As noted earlier, we obtain normal forms by evaluating then quoting.
nf : β X Y β Expr X Y β Hom β¦ X β§β β¦ Y β§β
nf X Y e = reflect X Y (eval e vid)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.
vhom-sound : β X Y β (f : Hom β¦ X β§β β¦ Y β§β) β reflect X Y (vhom f) β‘ f
vhom-sound X (Y βΆββΆ Z) f =
β¨ reflect X Y (vhom (Οβ β f)) , reflect X Z (vhom (Οβ β f)) β© β‘β¨ apβ β¨_,_β© (vhom-sound X Y (Οβ β f)) (vhom-sound X Z (Οβ β f)) β©
β¨ Οβ β f , Οβ β f β© β‘Λβ¨ β¨β©-unique refl refl β©
f β
vhom-sound X βΆ x βΆ f = reflNext, 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 π.
vfst-sound : β X Y Z β (v : Value X (Y βΆββΆ Z)) β reflect X Y (vfst v) β‘ Οβ β reflect X (Y βΆββΆ Z) v
vfst-sound X Y Z (vhom f) =
reflect X Y (vhom (Οβ β f)) β‘β¨ vhom-sound X Y (Οβ β f) β©
Οβ β f β‘Λβ¨ reflβ©ββ¨ vhom-sound X (Y βΆββΆ Z) f β©
Οβ β reflect X (Y βΆββΆ Z) (vhom f) β
vfst-sound X Y Z (vpair v1 v2) =
reflect X Y v1 β‘Λβ¨ Οβββ¨β© β©
Οβ β β¨ (reflect X Y v1) , (reflect X Z v2) β© β
vsnd-sound : β X Y Z β (v : Value X (Y βΆββΆ Z)) β reflect X Z (vsnd v) β‘ Οβ β reflect X (Y βΆββΆ Z) v
vsnd-sound X Y Z (vhom f) =
reflect X Z (vhom (Οβ β f)) β‘β¨ vhom-sound X Z (Οβ β f) β©
Οβ β f β‘Λβ¨ reflβ©ββ¨ vhom-sound X (Y βΆββΆ Z) f β©
Οβ β reflect X (Y βΆββΆ Z) (vhom f) β
vsnd-sound X Y Z (vpair v1 v2) =
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.
sound-k : β X Y Z β (e : Expr Y Z) β (v : Value X Y)
β reflect X Z (eval e v) β‘ β¦ e β§β β reflect X Y v
sound-k X Y Y βΆidβΆ v = sym (idl _)
sound-k X Y Z (e1 βΆββΆ e2) v =
reflect X Z (eval e1 (eval e2 v)) β‘β¨ sound-k X _ Z e1 (eval e2 v) β©
β¦ e1 β§β β reflect X _ (eval e2 v) β‘β¨ reflβ©ββ¨ sound-k X Y _ e2 v β©
β¦ e1 β§β β β¦ e2 β§β β reflect X Y v β‘β¨ assoc _ _ _ β©
β¦ e1 βΆββΆ e2 β§β β reflect X Y v β
sound-k X (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 Y (Z1 βΆββΆ Z2) βΆβ¨ e1 , e2 β©βΆ v =
β¨ reflect X Z1 (eval e1 v) , reflect X Z2 (eval e2 v) β© β‘β¨ apβ β¨_,_β© (sound-k X Y Z1 e1 v) (sound-k X Y Z2 e2 v) β©
β¨ β¦ e1 β§β β reflect X Y v , β¦ e2 β§β β reflect X Y v β© β‘Λβ¨ β¨β©β _ β©
β¨ β¦ e1 β§β , β¦ e2 β§β β© β reflect X Y v β
sound-k X Y Z βΆ x βΆ v = vhom-sound X Z _The final soundness proof: normalizing an expression gives us the same morphism as naively interpreting the expression.
sound : β X Y β (e : Expr X Y) β nf X Y e β‘ β¦ e β§β
sound X Y e = sound-k X X Y e vid β elimr (vhom-sound X X id)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
solve : β X Y β (e1 e2 : Expr X Y) β nf X Y e1 β‘ nf X Y e2 β β¦ e1 β§β β‘ β¦ e2 β§β
solve X Y e1 e2 p = sym (sound X Y e1) ββ p ββ sound X Y e2Reflectionπ
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
X hmβ· Y hmβ· -- objects of product
_ hmβ· -- apex
_ hmβ· _ hmβ· -- projections
_ vβ· -- is-product record argument
args
pattern product-field X Y args =
_ hmβ· _ hmβ· _ hmβ· -- category args
X hmβ· Y hmβ· -- objects of product
_ vβ· -- product record argument
args
pattern category-field args = _ hmβ· _ hmβ· _ vβ· args
pattern βββ X Y =
def (quote Product.apex) (product-field X Y [])
pattern βidβ X =
def (quote Precategory.id) (category-field (X hβ· []))
pattern βββ X Y Z f g =
def (quote Precategory._β_) (category-field (X hβ· Y hβ· Z hβ· f vβ· g vβ· []))
pattern βΟββ X Y =
def (quote (Product.Οβ)) (product-field X Y [])
pattern βΟββ X Y =
def (quote (Product.Οβ)) (product-field X Y [])
pattern ββ¨β©β X Y Z f g =
def (quote (is-product.β¨_,_β©)) (is-product-field Y Z (X hβ· f vβ· g vβ· []))Next, we define some helpers to make constructing things in the NbE module easier.
mk-nbe-con : Name β List (Arg Term) β Term
mk-nbe-con con-name args =
con con-name (unknown hβ· unknown hβ· unknown hβ· unknown hβ· args)
mk-nbe-call : Term β Term β List (Arg Term) β List (Arg Term)
mk-nbe-call cat cart args = unknown hβ· unknown hβ· cat vβ· cart vβ· argsWe also define some helpers for building quoted calls to NbE.nf and NbE.solve.
βnfβ : Term β Term β Term β Term β Term β Term
βnfβ cat cart x y e =
def (quote NbE.nf) (mk-nbe-call cat cart (x vβ· y vβ· e vβ· []))
βsolveβ : Term β Term β Term β Term β Term β Term β Term
βsolveβ cat cart x y lhs rhs =
def (quote NbE.solve) $
mk-nbe-call cat cart (x vβ· y vβ· lhs vβ· rhs vβ· βreflβ vβ· [])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.
build-obj-expr : Term β Term
build-obj-expr (βββ X Y) =
con (quote NbE.βΆObβΆ._βΆββΆ_) (build-obj-expr X vβ· build-obj-expr Y vβ· [])
build-obj-expr X =
con (quote NbE.βΆObβΆ.βΆ_βΆ) (X vβ· [])
build-hom-expr : Term β Term
build-hom-expr (βidβ X) =
mk-nbe-con (quote NbE.Expr.βΆidβΆ) $
build-obj-expr X hβ· []
build-hom-expr (βββ X Y Z f g) =
mk-nbe-con (quote NbE.Expr._βΆββΆ_) $
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 (βΟββ X Y) =
mk-nbe-con (quote NbE.Expr.βΆΟββΆ) $
build-obj-expr X hβ· build-obj-expr Y hβ· []
build-hom-expr (βΟββ X Y) =
mk-nbe-con (quote NbE.Expr.βΆΟββΆ) $
build-obj-expr X hβ· build-obj-expr Y hβ· []
build-hom-expr (ββ¨β©β X Y Z f g) =
mk-nbe-con (quote NbE.Expr.βΆβ¨_,_β©βΆ) $
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 =
con (quote NbE.Expr.βΆ_βΆ) (f vβ· [])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.
dont-reduce : List Name
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.
get-objects : Term β TC (Term Γ Term)
get-objects tm = ((infer-type tm >>= normalise) >>= wait-just-a-bit) >>= Ξ» where
(def (quote Precategory.Hom) (category-field (x vβ· y vβ· []))) β
pure (x , y)
tp β
typeError $ strErr "Can't determine objects: " β· termErr tp β· []We also make some debugging macros, which are very useful for when you want to examine the exact quoted representations of objects/homs.
obj-repr-macro : β {o β} (π : Precategory o β) (cartesian : β X Y β Product π X Y) β Term β Term β TC β€
obj-repr-macro cat cart hom hole =
withReconstructed true $
withNormalisation false $
withReduceDefs (false , dont-reduce) $ do
(x , y) β get-objects hom
βxβ β build-obj-expr <$> normalise x
βyβ β build-obj-expr <$> normalise y
typeError $ strErr "Determined objects of " β· termErr hom β· strErr " to be\n " β·
termErr x β· strErr "\nAnd\n " β·
termErr y β· strErr "\nWith quoted representations\n " β·
termErr βxβ β· strErr "\nAnd\n " β·
termErr βyβ β· []
hom-repr-macro : β {o β} (π : Precategory o β) (cartesian : β X Y β Product π X Y) β Term β Term β TC β€
hom-repr-macro cat cart hom hole =
withReconstructed true $
withNormalisation false $
withReduceDefs (false , dont-reduce) $ do
(x , y) β get-objects hom
βxβ β build-obj-expr <$> normalise x
βyβ β build-obj-expr <$> normalise y
βhomβ β build-hom-expr <$> normalise hom
typeError $ strErr "The morphism\n " β·
termErr hom β· strErr "\nis represented by\n " β·
termErr βhomβ β· strErr "\nwith objects\n " β·
termErr βxβ β· strErr "\nAnd\n " β·
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.
simpl-macro : β {o β} (π : Precategory o β) (cartesian : β X Y β Product π X Y) β Term β Term β TC β€
simpl-macro cat cart hom hole =
withReconstructed true $
withNormalisation false $
withReduceDefs (false , dont-reduce) $ do
(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
unify hole (βnfβ βcatβ βcartβ βxβ βyβ βhomβ)Finally, we define the user-facing interface as a series of macros.
macro
products-obj-repr! : β {o β}
β (π : Precategory o β) (cartesian : β X Y β Product π X Y)
β Term β Term β TC β€
products-obj-repr! = Reflection.obj-repr-macro
products-repr! : β {o β}
β (π : Precategory o β) (cartesian : β X Y β Product π X Y)
β Term β Term β TC β€
products-repr! = Reflection.hom-repr-macro
products-simpl! : β {o β}
β (π : Precategory o β) (cartesian : β X Y β Product π X Y)
β Term β Term β TC β€
products-simpl! = Reflection.simpl-macromodule _ {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
products-worker : Term β TC β€
products-worker goal = withReconstructed true $ withNormalisation true $ withReduceDefs (false , dont-reduce) do
`h1 β wait-for-type =<< quoteTC h1
`h2 β quoteTC h2
`x β quoteTC x
`y β quoteTC y
βcartβ β quoteTC cart
let
βxβ = build-obj-expr `x
βyβ = build-obj-expr `y
βlhsβ = build-hom-expr `h1
βrhsβ = build-hom-expr `h2
unify goal (Reflection.βsolveβ unknown βcartβ βxβ βyβ βlhsβ βrhsβ) <|> do
βcatβ β quoteTC C
vlhs β normalise (βnfβ βcatβ βcartβ βxβ βyβ βlhsβ)
vrhs β normalise (βnfβ βcatβ βcartβ βxβ βyβ βrhsβ)
typeError
[ "Could not equate the following expressions:\n "
, termErr `h1 , "\nAnd\n " , termErr `h2
, "\nReflected expressions\n "
, termErr βlhsβ , "\nAnd\n " , termErr βrhsβ
, strErr "\nComputed normal forms\n "
, termErr vlhs , strErr "\nAnd\n " , termErr vrhs
]
products-wrapper : {@(tactic products-worker) p : h1 β‘ h2} β h1 β‘ h2
products-wrapper {p = p} = p
macro
products! : Term β TC β€
products! = flip unify (def (quote products-wrapper) [])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
test-Ξ· : β {X Y Z} β (f : Hom X (Y ββ Z))
β f β‘ β¨ Οβ β f , Οβ β f β©
test-Ξ· f = products! cartesian
test-Ξ²β : β {X Y Z} β (f : Hom X Y) β (g : Hom X Z)
β Οβ β β¨ f , g β© β‘ f
test-Ξ²β f g = products! cartesian
test-Ξ²β : β {X Y Z} β (f : Hom X Y) β (g : Hom X Z)
β Οβ β β¨ f , g β© β‘ g
test-Ξ²β f g = products! cartesian
test-β¨β©β : β {W X Y Z} β (f : Hom X Y) β (g : Hom X Z) β (h : Hom W X)
β β¨ f β h , g β h β© β‘ β¨ f , g β© β h
test-β¨β©β f g h = products! cartesian
-- If you don't have 'withReconstructed' on, this test will fail!
test-nested : β {W X Y Z} β (f : Hom W X) β (g : Hom W Y) β (h : Hom W Z)
β β¨ β¨ f , g β© , h β© β‘ β¨ β¨ f , g β© , h β©
test-nested {W} {X} {Y} {Z} f g h = products! cartesian
test-big : β {W X Y Z} β (f : Hom (W ββ X) (W ββ Y)) β (g : Hom (W ββ X) Z)
β (Οβ β β¨ f , g β©) β id β‘ id β β¨ Οβ , Οβ β© β f
test-big f g = products! cartesianIn 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.β©οΈ