๐Ÿฆ€ Functional Rust

247: Limits and Colimits

Difficulty: โญโญโญ Level: Category Theory Every data type you've ever written is either a limit or a colimit โ€” universal constructions that define types by their relationship to all other types.

The Problem This Solves

Why does `(A, B)` feel like the "natural" way to hold both an `A` and a `B`? Why is `Either<A, B>` the "natural" way to hold one or the other? Category theory answers this: tuples are products (binary limits) and Either-types are coproducts (binary colimits). The "natural" feeling comes from the universal property โ€” a formal statement that `(A, B)` is the unique type (up to isomorphism) that has projections to both `A` and `B` and factors through every other type that does too. Understanding limits and colimits unifies all of Rust's fundamental types under one framework. Products, tuples, the unit type `()`, structs, equalizers (filter-like operations), and pullbacks (joins) are all limits. Coproducts, enums, the Never type `!`, and pushouts are all colimits. They're duals of each other.

The Intuition

A limit is the "most specific" type that maps to a collection of types. It combines information from multiple types in the most general way possible. A colimit is the dual โ€” the "most general" type that has maps from a collection: Duality: every limit statement flips to a colimit statement by reversing all arrows. Product โ†” Coproduct. Terminal โ†” Initial. Equalizer โ†” Coequalizer. Pullback โ†” Pushout.

How It Works in Rust

// Product (binary limit): (A, B) with projections
fn proj1<A: Clone, B>(p: &(A, B)) -> A { p.0.clone() }
fn proj2<A, B: Clone>(p: &(A, B)) -> B { p.1.clone() }

// Universal property: any X -> A and X -> B factors through (A, B)
fn pair_of<X: Clone, A, B>(f: impl Fn(X)->A, g: impl Fn(X)->B) -> impl Fn(X)->(A,B) {
 move |x| (f(x.clone()), g(x))  // the unique factoring morphism
}

// Coproduct (binary colimit): Either<A, B> with injections
enum Coprod<A, B> { Inl(A), Inr(B) }

fn inl<A, B>(a: A) -> Coprod<A, B> { Coprod::Inl(a) }
fn inr<A, B>(b: B) -> Coprod<A, B> { Coprod::Inr(b) }

// Universal property: any f:A->X and g:B->X factors through Either
fn copair<A, B, X>(f: impl Fn(A)->X, g: impl Fn(B)->X) -> impl Fn(Coprod<A,B>)->X {
 move |e| match e {
     Coprod::Inl(a) => f(a),  // this IS the match โ€” it's the unique factoring morphism
     Coprod::Inr(b) => g(b),
 }
}

// Equalizer (limit of parallel arrows): filter where f(x) == g(x)
fn equaliser<A: Clone, B: PartialEq>(
 xs: &[A], f: impl Fn(&A)->B, g: impl Fn(&A)->B
) -> Vec<A> {
 xs.iter().filter(|x| f(x) == g(x)).cloned().collect()
}

// Pullback (limit of span A->C<-B): pairs where f(a) == g(b)
fn pullback<'a, A: Clone, B: Clone, C: PartialEq>(
 as_: &[A], bs: &'a [B], f: impl Fn(&A)->C, g: impl Fn(&B)->C
) -> Vec<(A, B)> {
 as_.iter().flat_map(|a|
     bs.iter().filter(|b| f(a) == g(b)).map(|b| (a.clone(), b.clone()))
 ).collect()
}

What This Unlocks

Key Differences

ConceptOCamlRust
ProductTuple `(a * b)` or record`(A, B)` or struct
Coproduct`type t = A of a \B of b``enum Coprod<A, B> { Inl(A), Inr(B) }`
Terminal`unit` / `()``()`
Initial / Never`'a` (empty type)`!` / `std::convert::Infallible`
Universal propertyProven by constructionEncoded in generic function signature
/// Limits and Colimits in Category Theory.
///
/// A limit is a universal construction that "combines" a diagram of objects
/// into a single object with projections.
/// A colimit is the dual โ€” it "combines" a diagram with injections.
///
/// In the category of types (Hask/Rust):
///
///   Limits:
///     Product     = (A, B)       โ€” binary limit of {A, B}
///     Equaliser   = { x | f(x) = g(x) }
///     Pullback    = { (a,b) | f(a) = g(b) }
///     Terminal    = ()
///
///   Colimits:
///     Coproduct   = Either<A,B>  โ€” binary colimit of {A, B}
///     Coequaliser = quotient
///     Pushout     = colimit of a span
///     Initial     = ! (Never / Void)
///
/// These are foundational โ€” every data type is a limit or colimit.

// โ”€โ”€ Products (Binary Limits) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Product introduction: given A and B, produce (A, B).
fn product<A, B>(a: A, b: B) -> (A, B) {
    (a, b)
}

/// Product projections (the "legs" of the limit cone).
fn proj1<A: Clone, B>(p: &(A, B)) -> A { p.0.clone() }
fn proj2<A, B: Clone>(p: &(A, B)) -> B { p.1.clone() }

/// Universal property of the product:
/// Any function `h: X -> (A, B)` factors uniquely as `product(f, g)` where
/// `f = proj1 . h` and `g = proj2 . h`.
fn pair_of<X: Clone, A, B>(
    f: impl Fn(X) -> A,
    g: impl Fn(X) -> B,
) -> impl Fn(X) -> (A, B) {
    move |x| (f(x.clone()), g(x))
}

// โ”€โ”€ Coproducts (Binary Colimits) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Coproduct type.
#[derive(Debug, Clone, PartialEq)]
pub enum Coprod<A, B> {
    Inl(A),
    Inr(B),
}

/// Coproduct injections (the "legs" of the colimit cocone).
fn inl<A, B>(a: A) -> Coprod<A, B> { Coprod::Inl(a) }
fn inr<A, B>(b: B) -> Coprod<A, B> { Coprod::Inr(b) }

/// Universal property of the coproduct:
/// Any two functions `f: A -> X` and `g: B -> X` give a unique
/// `Either<A,B> -> X` (the "copairing").
fn copair<A, B, X>(
    f: impl Fn(A) -> X,
    g: impl Fn(B) -> X,
) -> impl Fn(Coprod<A, B>) -> X {
    move |c| match c {
        Coprod::Inl(a) => f(a),
        Coprod::Inr(b) => g(b),
    }
}

// โ”€โ”€ Terminal object โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Terminal object = () : there is exactly one morphism A -> () for any A.
fn terminal<A>(_a: A) -> () {}

// โ”€โ”€ Initial object โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Initial object = Void (Never): there is exactly one morphism Void -> A,
/// but Void has no inhabitants.
/// In Rust: std::convert::Infallible
fn from_initial(v: std::convert::Infallible) -> i32 {
    // This function has no body reachable โ€” it's never called.
    match v {}
}

// โ”€โ”€ Equaliser (limit of a parallel pair) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Equaliser of f and g: all x where f(x) = g(x).
fn equaliser<A: Clone, B: PartialEq>(
    xs: &[A],
    f: impl Fn(&A) -> B,
    g: impl Fn(&A) -> B,
) -> Vec<A> {
    xs.iter().filter(|x| f(x) == g(x)).cloned().collect()
}

// โ”€โ”€ Pullback (limit of a cospan) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Pullback of f: A -> C and g: B -> C:
/// { (a, b) | f(a) = g(b) }
fn pullback<A: Clone, B: Clone, C: PartialEq>(
    as_: &[A],
    bs: &[B],
    f: impl Fn(&A) -> C,
    g: impl Fn(&B) -> C,
) -> Vec<(A, B)> {
    let mut result = Vec::new();
    for a in as_ {
        for b in bs {
            if f(a) == g(b) {
                result.push((a.clone(), b.clone()));
            }
        }
    }
    result
}

// โ”€โ”€ Coequaliser (colimit of a parallel pair) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Coequaliser of f and g: quotient that identifies f(x) with g(x).
/// We model this as a partition: elements with the same "equivalence class" are merged.
/// Representative: smallest element in each class (under mod-based grouping).
fn coequaliser_classes<A: Clone + Ord>(
    xs: &[A],
    f: impl Fn(&A) -> usize,
    g: impl Fn(&A) -> usize,
) -> Vec<(usize, Vec<A>)> {
    use std::collections::HashMap;
    let mut classes: HashMap<usize, Vec<A>> = HashMap::new();
    for x in xs {
        // Both f(x) and g(x) are in the same equivalence class
        let key = f(x).min(g(x));
        classes.entry(key).or_default().push(x.clone());
    }
    let mut result: Vec<(usize, Vec<A>)> = classes.into_iter().collect();
    result.sort_by_key(|(k, _)| *k);
    result
}

fn main() {
    println!("=== Limits and Colimits ===\n");
    println!("Limits: product, equaliser, pullback, terminal");
    println!("Colimits: coproduct, coequaliser, pushout, initial\n");

    // โ”€โ”€ Products โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    println!("โ”€โ”€ Products (Limits) โ”€โ”€\n");
    let p = product(3_i32, "hello");
    println!("product(3, \"hello\") = ({}, \"{}\")", proj1(&p), proj2(&p));

    let both = pair_of(|x: i32| x * 2, |x: i32| x + 10);
    let (a, b) = both(5);
    println!("pair_of(*2, +10) applied to 5 = ({}, {})", a, b);

    // โ”€โ”€ Coproducts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    println!("\nโ”€โ”€ Coproducts (Colimits) โ”€โ”€\n");
    let x: Coprod<i32, &str> = inl(42);
    let y: Coprod<i32, &str> = inr("hello");
    let describe = copair(
        |n: i32| format!("number {}", n),
        |s: &str| format!("string \"{}\"", s),
    );
    println!("inl(42): {}", describe(x));
    println!("inr(\"hello\"): {}", describe(y));

    // โ”€โ”€ Terminal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    println!("\nโ”€โ”€ Terminal Object โ”€โ”€\n");
    println!("terminal(42) = {:?}", terminal(42));
    println!("terminal(\"anything\") = {:?}", terminal("anything"));
    println!("There is exactly one morphism A -> () for any A.");

    // โ”€โ”€ Equaliser โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    println!("\nโ”€โ”€ Equaliser โ”€โ”€\n");
    let nums = vec![0_i32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12];
    let eq = equaliser(&nums, |x| x % 2, |x| x % 3);
    println!("equaliser (x mod 2 = x mod 3) on 0..12: {:?}", eq);
    // x mod 2 = x mod 3 for x in {0, 6, 12} (when both are 0)

    // โ”€โ”€ Pullback โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    println!("\nโ”€โ”€ Pullback โ”€โ”€\n");
    let evens = vec![2_i32, 4, 6, 8];
    let words = vec!["ab", "cd", "abcd", "x"];
    let pb = pullback(&evens, &words, |n| *n as usize, |s| s.len());
    println!("pullback (length) of evens and words: {:?}", pb);

    // โ”€โ”€ Coequaliser โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    println!("\nโ”€โ”€ Coequaliser โ”€โ”€\n");
    let vals = vec![1_i32, 2, 3, 4, 5, 6];
    let classes = coequaliser_classes(&vals, |x| *x as usize % 3, |x| (*x + 1) as usize % 3);
    println!("coequaliser (mod 3, mod 3 +1) classes:");
    for (k, v) in &classes {
        println!("  class {}: {:?}", k, v);
    }

    println!();
    println!("Universal properties:");
    println!("  Product:    any cone (f: X->A, g: X->B) factors through (Aร—B)");
    println!("  Coproduct:  any cocone (f: A->X, g: B->X) factors through (A+B)");
    println!("  These are the 'most efficient' ways to combine/split types.");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_product_projections() {
        let p = product(10_i32, "hello");
        assert_eq!(proj1(&p), 10);
        assert_eq!(proj2(&p), "hello");
    }

    #[test]
    fn test_pair_of() {
        let f = pair_of(|x: i32| x * 2, |x: i32| x + 1);
        assert_eq!(f(5), (10, 6));
    }

    #[test]
    fn test_coproduct_inl() {
        let x: Coprod<i32, &str> = inl(42);
        let r = copair(|n: i32| n * 2, |_: &str| 0)(x);
        assert_eq!(r, 84);
    }

    #[test]
    fn test_coproduct_inr() {
        let x: Coprod<i32, &str> = inr("hello");
        let r = copair(|_: i32| 0, |s: &str| s.len())(x);
        assert_eq!(r, 5);
    }

    #[test]
    fn test_equaliser() {
        let nums = vec![0_i32, 1, 2, 3, 4, 5, 6];
        let eq = equaliser(&nums, |x| x % 2, |x| x % 3);
        // mod 2 = mod 3 at 0 (0=0), 6 (0=0)
        assert!(eq.contains(&0));
        assert!(eq.contains(&6));
    }

    #[test]
    fn test_pullback() {
        let as_ = vec![2_i32, 4, 6];
        let bs = vec!["ab", "abcd", "x"];
        let pb = pullback(&as_, &bs, |n| *n as usize, |s| s.len());
        assert!(pb.contains(&(2, "ab")));
        assert!(pb.contains(&(4, "abcd")));
    }
}
(* Limits and colimits are universal constructions in category theory.
   Products = binary limits; Coproducts = binary colimits.
   In types: products = tuples, coproducts = sum types *)

(* Product = limit of discrete diagram {A, B} *)
(* Characterised by projections fst, snd *)
let product_intro a b = (a, b)
let fst (a, _) = a
let snd (_, b) = b

(* Universality: any cone factors uniquely through the product *)
let pair_of f g x = (f x, g x)

(* Coproduct = colimit of discrete diagram {A, B} *)
type ('a, 'b) coprod = Inl of 'a | Inr of 'b

let coprod_inl a = Inl a
let coprod_inr b = Inr b

(* Universality: any cocone factors through the coproduct *)
let coprod_elim f g = function Inl a -> f a | Inr b -> g b

(* Equaliser = limit of parallel arrows *)
(* { x | f(x) = g(x) } *)
let equaliser f g lst = List.filter (fun x -> f x = g x) lst

(* Coequaliser = colimit *)
(* Quotient: identify elements where f(x) = f(y) -> same class *)
let coequaliser f lst =
  let module M = Map.Make(Int) in
  List.fold_left (fun m x -> M.add (f x) x m) M.empty lst
  |> M.bindings |> List.map snd

let () =
  Printf.printf "product (3,4): fst=%d snd=%d\n" (fst (3,4)) (snd (3,4));

  let both = pair_of (fun x -> x * 2) (fun x -> x + 10) in
  Printf.printf "pair_of: %s\n" (let (a,b) = both 5 in Printf.sprintf "(%d,%d)" a b);

  let sum_or_str = coprod_elim string_of_int (fun s -> s ^ "!") in
  Printf.printf "coprod Inl 42: %s\n" (sum_or_str (Inl 42));
  Printf.printf "coprod Inr hi: %s\n" (sum_or_str (Inr "hi"));

  let eq = equaliser (fun x -> x mod 2) (fun x -> x mod 3) [0;1;2;3;4;5;6;12] in
  Printf.printf "equaliser mod2=mod3: [%s]\n" (eq |> List.map string_of_int |> String.concat ";")