Replace hom-sets with hom-objects from a monoidal category V. Composition becomes a morphism in V, identity becomes a morphism from the monoidal unit. The payoff: preorders are Bool-enriched categories (hom = true/false), metric spaces are [0,∞]-enriched categories (hom = distance, composition = triangle inequality), and 2-categories are Cat-enriched categories (hom = functor categories). One definition, three fields of mathematics.
Why replace hom-sets?
In an ordinary category, the morphisms between two objects form a set. But sometimes you want more structure: a distance, a truth value, or an entire category of maps. Enriched categories replace the hom-set C(a,b) with a hom-object living in a monoidal category V. Composition, normally a function between sets, becomes a morphism in V. Identity, normally an element picked from a set, becomes a morphism from V's unit object. Everything is restated "point-free" so it works without naming individual morphisms.
Scheme
; An ordinary category has hom-SETS: finite collections of morphisms.; An enriched category has hom-OBJECTS from some monoidal category V.;; V must provide:; - a tensor product (⊗) to combine hom-objects; - a unit object (I) to source identity morphisms;; We'll build three examples:; 1. V = Bool (hom = true/false) => preorders; 2. V = [0,∞] (hom = distance) => metric spaces; 3. V = Set (hom = set) => ordinary categories; First: a monoidal category is (objects, tensor, unit)
(define (make-monoidal tensor unit)
(list tensor unit))
(define (monoidal-tensor m) (car m))
(define (monoidal-unit m) (cadr m))
; Bool as a monoidal category: tensor = AND, unit = #t
(define bool-monoidal (make-monoidal (lambda (a b) (and a b)) #t))
(display "Bool tensor: #t ⊗ #t = ")
(display ((monoidal-tensor bool-monoidal) #t#t)) (newline)
(display "Bool tensor: #t ⊗ #f = ")
(display ((monoidal-tensor bool-monoidal) #t#f)) (newline)
(display "Bool unit: ")
(display (monoidal-unit bool-monoidal))
Preorders as Bool-enriched categories
Enrich over Bool with AND as tensor and true as unit. The hom-object C(a,b) is just true or false: "can you get from a to b?" Composition requires C(b,c) AND C(a,b) ⇒ C(a,c), which is transitivity. Identity requires true ⇒ C(a,a), which is reflexivity. A preorder falls out of the definition for free.
Scheme
; A Bool-enriched category is a preorder.; hom(a,b) = #t means a ≤ b, #f means not.; Composition: hom(b,c) AND hom(a,b) => hom(a,c) [transitivity]; Identity: true => hom(a,a) [reflexivity]
(define (make-preorder leq?)
(list 'preorder leq?))
(define (preorder-hom p a b)
((cadr p) a b))
; The enriched composition law: if a≤b and b≤c then a≤c
(define (preorder-compose p a b c)
(and (preorder-hom p a b)
(preorder-hom p b c)))
; Example: divisibility preorder on positive integers; a ≤ b iff a divides b
(define divides-preorder
(make-preorder (lambda (a b) (= 0 (modulo b a)))))
; Check hom-objects (true/false)
(display "hom(2,6) = ") (display (preorder-hom divides-preorder 26)) (newline)
(display "hom(3,6) = ") (display (preorder-hom divides-preorder 36)) (newline)
(display "hom(4,6) = ") (display (preorder-hom divides-preorder 46)) (newline)
; Verify composition = transitivity: 2|6 and 6|12 => 2|12
(display "compose(2,6,12) = ")
(display (preorder-compose divides-preorder 2612)) (newline)
; Verify identity = reflexivity: hom(a,a) = #t
(display "hom(5,5) = ") (display (preorder-hom divides-preorder 55)) (newline)
; This IS the enriched category. No extra axioms needed.; The monoidal structure of Bool enforces preorder laws.
Python
# Bool-enriched category = preorder# hom(a,b) = True iff a <= bclass Preorder:
def __init__(self, leq):
self.leq = leq
def hom(self, a, b):
return self.leq(a, b)
def compose(self, a, b, c):
"""Enriched composition: hom(a,b) AND hom(b,c) => hom(a,c)"""return self.hom(a, b) and self.hom(b, c)
# Divisibility preorder
div_order = Preorder(lambda a, b: b % a == 0)
print(f"hom(2,6) = {div_order.hom(2, 6)}")
print(f"hom(4,6) = {div_order.hom(4, 6)}")
print(f"compose(2,6,12) = {div_order.compose(2, 6, 12)}")
print(f"hom(5,5) = {div_order.hom(5, 5)}")
Metric spaces as [0,∞]-enriched categories
Enrich over [0,∞] with addition as tensor and 0 as unit. The hom-object C(a,b) is the distance from a to b. Composition requires C(b,c) + C(a,b) ≥ C(a,c), which is the triangle inequality. Identity requires 0 ≥ C(a,a), so self-distance is zero. Metric space axioms emerge from the enrichment.
Scheme
; A [0,∞]-enriched category is a (generalized) metric space.; hom(a,b) = distance from a to b; Tensor = +, unit = 0;; Composition law: hom(a,b) + hom(b,c) >= hom(a,c); This IS the triangle inequality.; Identity law: 0 >= hom(a,a); So hom(a,a) = 0. Self-distance is zero.
(define (make-metric dist)
(list 'metric dist))
(define (metric-hom m a b)
((cadr m) a b))
; Check the enriched composition law (triangle inequality)
(define (metric-composition-ok? m a b c)
(>= (+ (metric-hom m a b) (metric-hom m b c))
(metric-hom m a c)))
; Example: points on the real line
(define real-line
(make-metric (lambda (a b) (abs (- a b)))))
(display "hom(1, 4) = ") (display (metric-hom real-line 14)) (newline)
(display "hom(4, 10) = ") (display (metric-hom real-line 410)) (newline)
(display "hom(1, 10) = ") (display (metric-hom real-line 110)) (newline)
; Triangle inequality: d(1,4) + d(4,10) >= d(1,10); 3 + 6 >= 9 ✓
(display "triangle(1,4,10)? ")
(display (metric-composition-ok? real-line 1410)) (newline)
; Identity: d(a,a) = 0
(display "hom(7,7) = ") (display (metric-hom real-line 77)) (newline)
; A tighter example: d(0,3) + d(3,5) = 3+2 = 5 >= d(0,5) = 5
(display "triangle(0,3,5)? ")
(display (metric-composition-ok? real-line 035))
; Equality holds — the shortest path goes through the midpoint.
Python
# [0,∞]-enriched category = metric space# Tensor = addition, unit = 0class Metric:
def __init__(self, dist):
self.dist = dist
def hom(self, a, b):
return self.dist(a, b)
def triangle_ok(self, a, b, c):
"""Enriched composition: d(a,b) + d(b,c) >= d(a,c)"""return self.hom(a, b) + self.hom(b, c) >= self.hom(a, c)
# Real line metric
real_line = Metric(lambda a, b: abs(a - b))
print(f"d(1,4) = {real_line.hom(1, 4)}")
print(f"d(4,10) = {real_line.hom(4, 10)}")
print(f"d(1,10) = {real_line.hom(1, 10)}")
print(f"triangle(1,4,10)? {real_line.triangle_ok(1, 4, 10)}")
print(f"d(7,7) = {real_line.hom(7, 7)}")
The enriched composition law
In an ordinary category, composition is a function C(b,c) × C(a,b) → C(a,c). In a V-enriched category, it is a morphism in V: C(b,c) ⊗ C(a,b) → C(a,c). For Bool, this is AND ⇒ hom. For [0,∞], this is + ≥ hom. Let's verify both in one framework.
Scheme
; Unified enriched composition check.; Given a monoidal category V and a hom-function,; verify the composition law:; tensor(hom(b,c), hom(a,b)) => hom(a,c);; For Bool: AND(hom(b,c), hom(a,b)) implies hom(a,c); For [0,∞]: hom(a,b) + hom(b,c) >= hom(a,c); Bool version: composition = implication
(define (bool-compose-ok? hom a b c)
; If hom(a,b) AND hom(b,c), then hom(a,c) must hold
(if (and (hom a b) (hom b c))
(hom a c) ; must be #t#t)) ; vacuously true; Metric version: composition = triangle inequality
(define (metric-compose-ok? hom a b c)
(>= (+ (hom a b) (hom b c))
(hom a c)))
; Test Bool (divisibility)
(define (divides? a b) (= 0 (modulo b a)))
(display "Bool composition checks (divisibility):
")
(display " 2|6 and 6|12 => 2|12? ")
(display (bool-compose-ok? divides? 2612)) (newline)
(display " 3|6 and 6|18 => 3|18? ")
(display (bool-compose-ok? divides? 3618)) (newline)
; Test metric (real line)
(define (real-dist a b) (abs (- a b)))
(display "
Metric composition checks (triangle inequality):
")
(display " d(0,3)+d(3,7) >= d(0,7)? ")
(display (metric-compose-ok? real-dist 037)) (newline)
(display " 3 + 4 = 7 >= 7 ✓
")
; 2D Euclidean distance — triangle inequality is strict
(define (dist-2d p q)
(sqrt (+ (expt (- (car p) (car q)) 2)
(expt (- (cdr p) (cdr q)) 2))))
(define A (cons 00))
(define B (cons 30))
(define C (cons 34))
(display "
2D triangle inequality:
")
(display " d(A,B) = ") (display (dist-2d A B)) (newline)
(display " d(B,C) = ") (display (dist-2d B C)) (newline)
(display " d(A,C) = ") (display (dist-2d A C)) (newline)
(display " 3 + 4 = 7 >= 5? ")
(display (metric-compose-ok? dist-2d A B C))
; Strict inequality: going through B is longer than the direct path.
2-categories as Cat-enriched categories
When V = Cat (the category of small categories), hom-objects are entire categories. Morphisms in the hom-category are 2-cells: natural transformations between functors. The canonical example: Cat itself is enriched over Cat, with functor categories as hom-objects. This gives 2-categories: objects, 1-morphisms (functors), and 2-morphisms (natural transformations).
Scheme
; Cat-enriched category = 2-category.; hom(A,B) is a CATEGORY, not a set.; Objects of hom(A,B) = functors A -> B (1-morphisms); Morphisms of hom(A,B) = natural transformations (2-morphisms);; We can't build full functor categories in Scheme,; but we can model the structure: hom-objects that are; themselves categories with vertical composition.; A 2-cell: a natural transformation between two functors
(define (make-2-cell name source target component)
(list name source target component))
(define (two-cell-name nt) (car nt))
(define (two-cell-source nt) (cadr nt))
(define (two-cell-target nt) (caddr nt))
(define (two-cell-at nt x) ((cadddr nt) x))
; Vertical composition of 2-cells (within a hom-category); If α : F => G and β : G => H, then β∘α : F => H
(define (vertical-compose beta alpha)
(make-2-cell
(string-append (two-cell-name beta) "." (two-cell-name alpha))
(two-cell-source alpha)
(two-cell-target beta)
(lambda (x) (list 'compose (two-cell-at beta x) (two-cell-at alpha x)))))
; Example: three "functors" (just named mappings for illustration); F: x -> x+1, G: x -> x+2, H: x -> x+3; α : F => G (component at x: the morphism x+1 -> x+2); β : G => H (component at x: the morphism x+2 -> x+3)
(define alpha
(make-2-cell "α" 'F 'G (lambda (x) (list '-> (+ x 1) (+ x 2)))))
(define beta
(make-2-cell "β" 'G 'H (lambda (x) (list '-> (+ x 2) (+ x 3)))))
(define beta-alpha (vertical-compose beta alpha))
(display "α at 5: ") (display (two-cell-at alpha 5)) (newline)
(display "β at 5: ") (display (two-cell-at beta 5)) (newline)
(display "β∘α at 5: ") (display (two-cell-at beta-alpha 5)) (newline)
(display "β∘α source: ") (display (two-cell-source beta-alpha)) (newline)
(display "β∘α target: ") (display (two-cell-target beta-alpha))
; The hom-category hom(A,B) has its own composition. That's 2-categorical.
Self-enrichment: Set over Set
A closed symmetric monoidal category can enrich over itself. Set with cartesian product is the prototype: the internal hom [a,b] is the function set b^a. Composition uses the universal property of the exponential. This is why ordinary categories (enriched over Set) feel "native" — Set is self-enriching.
Scheme
; Set enriched over Set = ordinary category.; The internal hom [a,b] is the set of functions a -> b.; Composition: [b,c] × [a,b] -> [a,c]; is just ordinary function composition.; Model: finite sets as lists, functions as association lists
(define (make-fn domain mapping)
(list domain mapping))
(define (apply-fn f x)
(let ((pair (assoc x (cadr f))))
(if pair (cdr pair) 'undefined)))
; Two functions to compose
(define f (make-fn '(123) '((1 . a) (2 . b) (3 . c))))
(define g (make-fn '(a b c) '((a . x) (b . y) (c . z))))
; Enriched composition: build the composite function
(define (compose-fn g f)
(make-fn
(car f)
(map (lambda (pair)
(cons (car pair) (apply-fn g (cdr pair))))
(cadr f))))
(define gf (compose-fn g f))
(display "f(1)=") (display (apply-fn f 1))
(display ", g(a)=") (display (apply-fn g 'a))
(display ", (g∘f)(1)=") (display (apply-fn gf 1)) (newline)
(display "f(2)=") (display (apply-fn f 2))
(display ", g(b)=") (display (apply-fn g 'b))
(display ", (g∘f)(2)=") (display (apply-fn gf 2)) (newline)
; The enriched identity: unit I -> [a,a] picks out id_a
(define (enriched-id domain)
(make-fn domain (map (lambda (x) (cons x x)) domain)))
(define id-123 (enriched-id '(123)))
(display "id(1)=") (display (apply-fn id-123 1))
(display ", id(2)=") (display (apply-fn id-123 2))
; Self-enrichment makes the internal structure explicit.
Python
# Set enriched over Set: ordinary function composition# Internal hom [a,b] = set of all functions a -> b
f = {1: "a", 2: "b", 3: "c"}
g = {"a": "x", "b": "y", "c": "z"}
# Enriched composition: [b,c] × [a,b] -> [a,c]
gf = {k: g[v] for k, v in f.items()}
print(f"f(1)={f[1]}, g(a)={g['a']}, (g∘f)(1)={gf[1]}")
print(f"f(2)={f[2]}, g(b)={g['b']}, (g∘f)(2)={gf[2]}")
# Enriched identity: unit -> [a,a]
id_fn = {x: x for x in [1, 2, 3]}
print(f"id(1)={id_fn[1]}, id(2)={id_fn[2]}")
The blog post develops enriched categories by first recapping monoidal categories, then giving the point-free reformulation of composition and identity. This page skips the monoidal recap (covered in Ch. 16) and jumps to the three key examples: Bool, [0,∞], and Cat. The fact that the same definition spans preorders, metric spaces, and 2-categories is a case study in how different mathematical communities share overlapping formalisms under different names. The blog post also discusses enriched functors (morphisms of hom-objects that preserve composition) and self-enrichment of closed monoidal categories. Self-enrichment is shown here for Set; the general construction uses the internal hom adjunction. Weighted limits, mentioned briefly in the blog post as a generalization of ordinary limits to the enriched setting, are omitted here since they require enriched presheaves. The Lawvere metric space characterization allows asymmetric distances and does not require the separation axiom (d(a,b) = 0 does not imply a = b), making it more general than the standard metric space definition.
Ready for the real thing? Read the blog post. Start with the monoidal category recap, then see how hom-sets become hom-objects.