ExamplesBy LevelBy TopicLearning Paths
219 Expert

Hylomorphism — Ana then Cata, Fused

Functional Programming

Tutorial

The Problem

A hylomorphism (hylo) is the composition of an anamorphism followed by a catamorphism: unfold a seed into a structure, then immediately fold the structure. The profound optimization: the intermediate structure need never be materialized. hylo fuses the unfold and fold into a single pass, eliminating intermediate allocation. Mergesort is a canonical hylomorphism: unfold the array into a tree, fold the tree by merging sorted sublists.

🎯 Learning Outcomes

  • • Understand hylomorphisms as fused ana + cata with no intermediate structure
  • • Learn the fusion optimization that eliminates intermediate allocation
  • • See mergesort as the canonical hylomorphism example
  • • Understand how hylo generalizes fold_left (which is a hylomorphism over the natural numbers)
  • Code Example

    fn hylo<S, A>(alg: &dyn Fn(ListF<A>) -> A, coalg: &dyn Fn(S) -> ListF<S>, seed: S) -> A {
        alg(coalg(seed).map(|s| hylo(alg, coalg, s)))
    }

    Key Differences

  • Fusion: hylo avoids the intermediate Fix-wrapped structure; ana + cata would allocate and then free it — hylo is the allocation-free composition.
  • Mergesort structure: Both implementations produce the same O(n log n) mergesort; the hylo framing makes the structure/composition explicit.
  • Streaming: hylo over lazy data structures enables stream fusion — the foundation of Haskell's stream-fusion and Rust's zero-allocation iterator chains.
  • Category theory: hylo = cata . ana; fusion is justified by the hylo lemma in category theory (a hylo decomposition always exists).
  • OCaml Approach

    OCaml's hylo:

    let rec hylo coalg alg seed =
      alg (map_list_f (hylo coalg alg) (coalg seed))
    

    This is the fused version — coalg produces the shape, map_list_f (hylo coalg alg) recursively processes children, alg combines. OCaml's let rec handles the mutual recursion naturally. OCaml's mergesort is conventionally written as a two-phase algorithm; expressing it as hylo is educational.

    Full Source

    #![allow(clippy::all)]
    // Example 219: Hylomorphism — Ana then Cata, Fused
    
    // hylo: unfold a seed, then fold the result. No intermediate structure!
    
    #[derive(Debug)]
    enum ListF<A> {
        NilF,
        ConsF(i64, A),
    }
    
    impl<A> ListF<A> {
        fn map<B>(self, f: impl Fn(A) -> B) -> ListF<B> {
            match self {
                ListF::NilF => ListF::NilF,
                ListF::ConsF(x, a) => ListF::ConsF(x, f(a)),
            }
        }
    }
    
    fn hylo<S, A>(alg: &dyn Fn(ListF<A>) -> A, coalg: &dyn Fn(S) -> ListF<S>, seed: S) -> A {
        alg(coalg(seed).map(|s| hylo(alg, coalg, s)))
    }
    
    // Approach 1: Factorial
    fn factorial(n: i64) -> i64 {
        hylo(
            &|l| match l {
                ListF::NilF => 1,
                ListF::ConsF(n, acc) => n * acc,
            },
            &|n| {
                if n <= 0 {
                    ListF::NilF
                } else {
                    ListF::ConsF(n, n - 1)
                }
            },
            n,
        )
    }
    
    // Approach 2: Sum of range
    fn sum_range(n: i64) -> i64 {
        hylo(
            &|l| match l {
                ListF::NilF => 0,
                ListF::ConsF(x, acc) => x + acc,
            },
            &|n| {
                if n <= 0 {
                    ListF::NilF
                } else {
                    ListF::ConsF(n, n - 1)
                }
            },
            n,
        )
    }
    
    // Approach 3: Merge sort via tree hylo
    #[derive(Debug)]
    enum TreeF<A> {
        LeafF(i64),
        BranchF(A, A),
    }
    
    impl<A> TreeF<A> {
        fn map<B>(self, f: impl Fn(A) -> B) -> TreeF<B> {
            match self {
                TreeF::LeafF(n) => TreeF::LeafF(n),
                TreeF::BranchF(l, r) => TreeF::BranchF(f(l), f(r)),
            }
        }
    }
    
    fn hylo_tree<S, A>(alg: &dyn Fn(TreeF<A>) -> A, coalg: &dyn Fn(S) -> TreeF<S>, seed: S) -> A {
        alg(coalg(seed).map(|s| hylo_tree(alg, coalg, s)))
    }
    
    fn merge(xs: &[i64], ys: &[i64]) -> Vec<i64> {
        let (mut i, mut j) = (0, 0);
        let mut result = Vec::with_capacity(xs.len() + ys.len());
        while i < xs.len() && j < ys.len() {
            if xs[i] <= ys[j] {
                result.push(xs[i]);
                i += 1;
            } else {
                result.push(ys[j]);
                j += 1;
            }
        }
        result.extend_from_slice(&xs[i..]);
        result.extend_from_slice(&ys[j..]);
        result
    }
    
    fn merge_sort(xs: Vec<i64>) -> Vec<i64> {
        if xs.is_empty() {
            return vec![];
        }
        hylo_tree(
            &|t| match t {
                TreeF::LeafF(n) => vec![n],
                TreeF::BranchF(l, r) => merge(&l, &r),
            },
            &|xs: Vec<i64>| {
                if xs.len() <= 1 {
                    TreeF::LeafF(xs[0])
                } else {
                    let mid = xs.len() / 2;
                    TreeF::BranchF(xs[..mid].to_vec(), xs[mid..].to_vec())
                }
            },
            xs,
        )
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_factorial() {
            assert_eq!(factorial(6), 720);
        }
        #[test]
        fn test_sum_range() {
            assert_eq!(sum_range(5), 15);
        }
        #[test]
        fn test_merge_sort() {
            assert_eq!(merge_sort(vec![9, 7, 5, 3, 1]), vec![1, 3, 5, 7, 9]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_factorial() {
            assert_eq!(factorial(6), 720);
        }
        #[test]
        fn test_sum_range() {
            assert_eq!(sum_range(5), 15);
        }
        #[test]
        fn test_merge_sort() {
            assert_eq!(merge_sort(vec![9, 7, 5, 3, 1]), vec![1, 3, 5, 7, 9]);
        }
    }

    Deep Comparison

    Comparison: Example 219 — Hylomorphism

    hylo Definition

    OCaml

    let rec hylo alg coalg seed =
      alg (map_f (hylo alg coalg) (coalg seed))
    

    Rust

    fn hylo<S, A>(alg: &dyn Fn(ListF<A>) -> A, coalg: &dyn Fn(S) -> ListF<S>, seed: S) -> A {
        alg(coalg(seed).map(|s| hylo(alg, coalg, s)))
    }
    

    Factorial

    OCaml

    let factorial n = hylo
      (function NilF -> 1 | ConsF (n, acc) -> n * acc)
      (fun n -> if n <= 0 then NilF else ConsF (n, n - 1))
      n
    

    Rust

    fn factorial(n: i64) -> i64 {
        hylo(
            &|l| match l { ListF::NilF => 1, ListF::ConsF(n, acc) => n * acc },
            &|n| if n <= 0 { ListF::NilF } else { ListF::ConsF(n, n - 1) },
            n,
        )
    }
    

    Merge Sort (Tree Hylo)

    OCaml

    let merge_sort xs = hylo_tree merge_alg split_coalg xs
    

    Rust

    fn merge_sort(xs: Vec<i64>) -> Vec<i64> {
        if xs.is_empty() { return vec![]; }
        hylo_tree(&merge_alg, &split_coalg, xs)
    }
    

    Exercises

  • Implement sum_range(n) as hylo(range_coalg, sum_alg) — sum all integers from 1 to n without building the list.
  • Verify that mergesort via hylo produces the same results as sort on 1000 random integers.
  • Measure allocation: compare ana + cata (materializing the tree) vs. hylo (fused) for mergesort on 10,000 elements.
  • Open Source Repos