Enriched Category Theory¶
Ends and Coends¶
An end \(\int_X F(X, X)\) is a generalized intersection: the limit over the diagonal. Dually, a coend \(\int^X F(X, Y)\) is a generalized union: the colimit over the diagonal.
Formally, an end is an object \(E\) equipped with projections \(\pi_X: E \to F(X, X)\) such that for all \(f: X \to Y\), the diagram
commutes (naturality in the diagonals).
In quivers:
from quivers.enriched.ends_coends import end, coend
from quivers.core.objects import FinSet
from quivers.core.morphisms import morphism
# A diagram F: C^op × C → Set
# For finite sets, we compute the end as an intersection of fibers
X = FinSet("X", 3)
Y = FinSet("Y", 4)
# Functor F(A, B) that takes two objects and returns a morphism
def F(A, B):
return morphism(A, B)
# End: ∫_Z F(Z, Z)
e = end({X, Y}, F)
# Coend: ∫^Z F(Z, Z) (geometric realization / coequalizer)
ce = coend({X, Y}, F)
Yoneda Lemma (via Ends)¶
The Yoneda lemma states:
where \(y_A\) is the representable presheaf and \(F\) is any presheaf. This is an isomorphism of natural transformations and elements.
from quivers.enriched.yoneda import yoneda_lemma
# Compute Yoneda isomorphism
A = FinSet("A", 3)
F = ... # presheaf
# Naturals from representable to F correspond to elements of F(A)
iso = yoneda_lemma(A, F)
assert iso.shape == F(A).shape
Kan Extensions¶
A Kan extension is a universal way to extend a functor \(F: \mathcal{A} \to \mathcal{B}\) along an inclusion \(i: \mathcal{A} \to \mathcal{C}\).
The left Kan extension \(\text{Lan}_i F: \mathcal{C} \to \mathcal{B}\) is the initial such extension. The right Kan extension \(\text{Ran}_i F\) is terminal.
Left Kan extension is computed via a colimit:
In quivers:
from quivers.enriched.kan_extensions import left_kan, right_kan, Projection, Inclusion
# Define the inclusion i: A → C
A = [FinSet("A", 2), FinSet("B", 3)]
C = [A[0], A[1], FinSet("C", 4)]
inclusion = Inclusion(A, C)
# Define functor F: A → D
def F(obj):
return morphism(obj, FinSet("target", 5))
# Left Kan extension: extends F along inclusion
left_ext = left_kan(F, inclusion)
# Right Kan extension
right_ext = right_kan(F, inclusion)
# Query the extension
ext_C = left_ext(C[2]) # Value at new object
Weighted Limits and Colimits¶
A weighted limit is a limit parameterized by a weight object (a presheaf or profunctor). If \(W: \mathcal{C}^\text{op} \to \mathcal{V}\) is a weight and \(F: \mathcal{C} \to \mathcal{D}\) is a diagram, the weighted limit \(\{W, F\}\) is defined by the universal property:
from quivers.enriched.weighted_limits import (
weighted_limit, Weight, Diagram, representable_weight
)
# Weight: W: C^op → V
W = representable_weight(C, X) # representable by object X
# Diagram: F: C → D
F = Diagram({obj: morphism(obj, D) for obj in C})
# Weighted limit
limit = weighted_limit(W, F)
# Projections to diagram objects
for obj in C:
proj = limit.projection(obj)
assert proj.codomain == F[obj].codomain
Weighted colimits are the dual. Weighted limits generalize: - Ordinary limits (weight = terminal presheaf) - Cotensor products (weight = hom functor) - Powers and copowers (weight = constant presheaf)
Profunctors¶
A profunctor (or distributor) \(P: \mathcal{A} \nrightarrow \mathcal{B}\) is a \(\mathcal{V}\)-valued bimodule, i.e., a functor \(P: \mathcal{B}^\text{op} \times \mathcal{A} \to \mathcal{V}\).
from quivers.enriched.profunctors import Profunctor
# Profunctor P: A ↛ B
# For objects a ∈ A, b ∈ B, provides a value P(b, a) ∈ V
class MyProfileunctor(Profunctor):
def __call__(self, b, a):
"""P(b, a) ∈ V"""
pass
# Composition of profunctors (Kleisli category of *-)
# (Q ∘ P)(c, a) = ∫^b P(b, a) ⊗ Q(c, b)
Morphisms in \(\mathcal{C}\) embed as hom-profunctors: \(\hom_\mathcal{C}(-,-): \mathcal{C}^\text{op} \times \mathcal{C} \to \mathcal{V}\).
Yoneda Embedding and Representability¶
The Yoneda embedding \(y: \mathcal{C} \to [\mathcal{C}^\text{op}, \mathcal{V}]\) sends an object \(A\) to the representable presheaf \(y_A = \hom(-,A)\).
A presheaf \(F\) is representable if \(F \cong y_A\) for some object \(A\).
from quivers.enriched.yoneda import (
yoneda_embedding, representable_profunctor, verify_yoneda_fully_faithful
)
# Yoneda embedding of an object
A = FinSet("A", 3)
presheaf = yoneda_embedding(A)
# Check that yoneda_embedding is fully faithful
is_ff = verify_yoneda_fully_faithful(C)
assert is_ff
Day Convolution¶
The Day convolution \(F *_\otimes G\) on presheaves (with monoidal codomain) is defined by:
Day convolution lifts the monoidal structure from the base category to presheaves.
from quivers.enriched.day_convolution import (
day_convolution, day_unit, day_convolution_profunctors
)
# Presheaves F, G on a monoidal category
F = ... # presheaf
G = ... # presheaf
# Day convolution
FG = day_convolution(F, G)
# The unit object for Day convolution
unit = day_unit()
Optics¶
An optic \(p: (S, T) \to (A, B)\) is a pair of morphisms satisfying:
Optics compose, and they generalize lenses, prisms, adapters, and other bidirectional morphisms.
Lens¶
A lens focuses on a component of a product:
from quivers.enriched.optics import Lens
# Lens: (S, T) → (A, B)
# A lens picks out a part A of a whole S and shows how to update
# the whole T given a new value B.
S = FinSet("S", 3)
A = FinSet("A", 2)
# Lens: focuses on first component of a product
lens = Lens.get_set(
get=morphism(S * A, A), # project A from S
set=morphism(S * A, S), # update S with new A
)
Prism¶
A prism focuses on a case of a coproduct:
from quivers.enriched.optics import Prism
# Prism: (S, T) → (A, B)
# A prism tries to match a case and extract the value
S = FinSet("S", 5) # sum type
A = FinSet("A", 2) # matched case
prism = Prism.match_build(
match=morphism(S, A), # extract case
build=morphism(A, S), # rebuild
)
Adapter¶
An adapter is a bijection between types:
from quivers.enriched.optics import Adapter
# Adapter: isomorphism between S and A
fwd = morphism(S, A)
rev = morphism(A, S)
adapter = Adapter(fwd, rev)
Grate¶
A grate distributes an update function:
from quivers.enriched.optics import Grate
# Grate: entire structure is "writable"
grate = Grate(morphism(...), ...)
Optics compose with the ∘ operator:
from quivers.enriched.optics import compose_optics
p1 = lens # S ↔ A
p2 = prism # A ↔ C
composed = compose_optics(p1, p2) # S ↔ C