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
ana + cata with no intermediate structurehylo 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
hylo avoids the intermediate Fix-wrapped structure; ana + cata would allocate and then free it — hylo is the allocation-free composition.hylo framing makes the structure/composition explicit.hylo over lazy data structures enables stream fusion — the foundation of Haskell's stream-fusion and Rust's zero-allocation iterator chains.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
sum_range(n) as hylo(range_coalg, sum_alg) — sum all integers from 1 to n without building the list.hylo produces the same results as sort on 1000 random integers.ana + cata (materializing the tree) vs. hylo (fused) for mergesort on 10,000 elements.