ExamplesBy LevelBy TopicLearning Paths
222 Expert

Histomorphism — Cata with Full History

Functional Programming

Tutorial

The Problem

Sometimes a fold needs not just the immediate child's result, but the full history of all previous results. Fibonacci is the classic example: fib(n) = fib(n-1) + fib(n-2) — the current result depends on two previous results. A catamorphism can only see the immediately adjacent result. A histomorphism provides access to all previous results via a "Cofree" annotation: each node carries its folded result and a pointer to the previous annotation, forming a linked history.

🎯 Learning Outcomes

  • • Understand histomorphisms as catamorphisms with access to the full fold history
  • • Learn the Cofree type: Cofree<F, A> { value: A, sub: F<Cofree<F, A>> }
  • • See how Fibonacci in O(n) is a natural histomorphism over natural numbers
  • • Understand the performance benefit: histo computes Fibonacci in O(n) vs. naive recursion O(2ⁿ)
  • Code Example

    struct Cofree<A> {
        head: A,
        tail: Box<NatF<Cofree<A>>>,
    }

    Key Differences

  • Implicit memoization: histo achieves O(n) Fibonacci through the Cofree structure, not through an explicit HashMap — the history IS the cache.
  • Cofree comonad: Cofree<F, A> is the Cofree comonad over functor F (example 245) — the recursion scheme and comonad concepts are deeply connected.
  • Performance: Naive recursive Fibonacci is O(2ⁿ); histo Fibonacci is O(n) with O(n) stack space; iterative is O(n) with O(1) space.
  • History depth: histo gives access to ALL previous results; para gives access only to the immediate sub-structure.
  • OCaml Approach

    OCaml's histomorphism:

    type 'a cofree = Cofree of 'a * 'a cofree nat_f
    
    let rec histo alg (Fix nf) =
      let cf = map_nat_f (fun child ->
        let result = histo alg child in
        Cofree(result, map_nat_f (histo alg) nf)
      ) nf in
      alg cf
    

    OCaml's pattern matching on Cofree accesses the history naturally. The histo scheme computes Fibonacci without memoization tables.

    Full Source

    #![allow(clippy::all)]
    // Example 222: Histomorphism — Cata with Full History (Fibonacci in O(n))
    
    // histo: algebra sees all previous results via Cofree
    
    #[derive(Debug, Clone)]
    enum NatF<A> {
        ZeroF,
        SuccF(A),
    }
    
    impl<A> NatF<A> {
        fn map<B>(self, f: impl Fn(A) -> B) -> NatF<B> {
            match self {
                NatF::ZeroF => NatF::ZeroF,
                NatF::SuccF(a) => NatF::SuccF(f(a)),
            }
        }
        fn map_ref<B>(&self, f: impl Fn(&A) -> B) -> NatF<B> {
            match self {
                NatF::ZeroF => NatF::ZeroF,
                NatF::SuccF(a) => NatF::SuccF(f(a)),
            }
        }
    }
    
    // Cofree: result + history
    #[derive(Debug, Clone)]
    struct Cofree<A> {
        head: A,
        tail: Box<NatF<Cofree<A>>>,
    }
    
    impl<A> Cofree<A> {
        fn new(head: A, tail: NatF<Cofree<A>>) -> Self {
            Cofree {
                head,
                tail: Box::new(tail),
            }
        }
    }
    
    #[derive(Debug, Clone)]
    struct FixNat(Box<NatF<FixNat>>);
    
    fn zero() -> FixNat {
        FixNat(Box::new(NatF::ZeroF))
    }
    fn succ(n: FixNat) -> FixNat {
        FixNat(Box::new(NatF::SuccF(n)))
    }
    fn nat(n: u32) -> FixNat {
        (0..n).fold(zero(), |acc, _| succ(acc))
    }
    
    // histo: build cofree bottom-up, algebra sees history chain
    fn histo<A: Clone>(alg: &dyn Fn(NatF<Cofree<A>>) -> A, fix: &FixNat) -> A {
        histo_build(alg, fix).head
    }
    
    fn histo_build<A: Clone>(alg: &dyn Fn(NatF<Cofree<A>>) -> A, fix: &FixNat) -> Cofree<A> {
        let layer = fix.0.map_ref(|child| histo_build(alg, child));
        let result = alg(layer.clone());
        Cofree::new(result, layer)
    }
    
    // NatF derives Clone, which covers NatF<Cofree<A>> when A: Clone
    
    // Approach 1: Fibonacci — algebra looks back 2 steps
    fn fib_alg(n: NatF<Cofree<u64>>) -> u64 {
        match n {
            NatF::ZeroF => 0,
            NatF::SuccF(cf) => match cf.tail.as_ref() {
                NatF::ZeroF => 1,                       // fib(1) = 1
                NatF::SuccF(cf2) => cf.head + cf2.head, // fib(n-1) + fib(n-2)
            },
        }
    }
    
    // Approach 2: Tribonacci — looks back 3 steps
    fn trib_alg(n: NatF<Cofree<u64>>) -> u64 {
        match n {
            NatF::ZeroF => 0,
            NatF::SuccF(cf) => match cf.tail.as_ref() {
                NatF::ZeroF => 0, // trib(1) = 0
                NatF::SuccF(cf2) => match cf2.tail.as_ref() {
                    NatF::ZeroF => 1, // trib(2) = 1
                    NatF::SuccF(cf3) => cf.head + cf2.head + cf3.head,
                },
            },
        }
    }
    
    // Approach 3: General "look back n" pattern
    fn lucas_alg(n: NatF<Cofree<u64>>) -> u64 {
        match n {
            NatF::ZeroF => 2,
            NatF::SuccF(cf) => match cf.tail.as_ref() {
                NatF::ZeroF => 1,
                NatF::SuccF(cf2) => cf.head + cf2.head,
            },
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_fib_20() {
            assert_eq!(histo(&fib_alg, &nat(20)), 6765);
        }
        #[test]
        fn test_trib_7() {
            assert_eq!(histo(&trib_alg, &nat(7)), 13);
        }
        #[test]
        fn test_lucas_6() {
            assert_eq!(histo(&lucas_alg, &nat(6)), 18);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_fib_20() {
            assert_eq!(histo(&fib_alg, &nat(20)), 6765);
        }
        #[test]
        fn test_trib_7() {
            assert_eq!(histo(&trib_alg, &nat(7)), 13);
        }
        #[test]
        fn test_lucas_6() {
            assert_eq!(histo(&lucas_alg, &nat(6)), 18);
        }
    }

    Deep Comparison

    Comparison: Example 222 — Histomorphism

    Cofree Type

    OCaml

    type 'a cofree_nat = CF of 'a * 'a cofree_nat nat_f
    let head (CF (a, _)) = a
    let tail (CF (_, t)) = t
    

    Rust

    struct Cofree<A> {
        head: A,
        tail: Box<NatF<Cofree<A>>>,
    }
    

    Fibonacci Algebra (Look Back 2 Steps)

    OCaml

    let fib_alg = function
      | ZeroF -> 0
      | SuccF (CF (n1, ZeroF)) -> max 1 n1
      | SuccF (CF (n1, SuccF (CF (n2, _)))) -> n1 + n2
    

    Rust

    fn fib_alg(n: NatF<Cofree<u64>>) -> u64 {
        match n {
            NatF::ZeroF => 0,
            NatF::SuccF(cf) => match cf.tail.as_ref() {
                NatF::ZeroF => 1,
                NatF::SuccF(cf2) => cf.head + cf2.head,
            }
        }
    }
    

    histo Implementation

    OCaml

    let rec histo_simple alg (FixN f) =
      alg (map_nat (histo_build alg) f)
    and histo_build alg node =
      let result = histo_simple alg node in
      CF (result, map_nat (histo_build alg) (match node with FixN g -> g))
    

    Rust

    fn histo<A: Clone>(alg: &dyn Fn(NatF<Cofree<A>>) -> A, fix: &FixNat) -> A {
        histo_build(alg, fix).head
    }
    fn histo_build<A: Clone>(alg: &dyn Fn(NatF<Cofree<A>>) -> A, fix: &FixNat) -> Cofree<A> {
        let layer = fix.0.map_ref(|child| histo_build(alg, child));
        let result = alg(layer.clone());
        Cofree::new(result, layer)
    }
    

    Exercises

  • Implement Lucas numbers using histo: L(0)=2, L(1)=1, L(n)=L(n-1)+L(n-2).
  • Verify that histo_fibonacci(30) produces the correct result and is faster than the naive recursive version.
  • Implement a sliding-window maximum using histo that returns the max over the last k elements.
  • Open Source Repos