Anamorphism — Unfold to Build Recursive Structures
Functional Programming
Tutorial
The Problem
The dual of a catamorphism is the anamorphism (ana) — it unfolds a seed into a recursive structure. Where cata tears down a structure bottom-up, ana builds one top-down. unfold for lists is an anamorphism: starting from a seed, repeatedly apply a coalgebra to produce the next element and the new seed, until reaching the base case. This is how infinite streams, number sequences, and tree generation are expressed functionally.
🎯 Learning Outcomes
seed -> F<seed> drives top-down unfoldingCode Example
fn ana<S>(coalg: &dyn Fn(S) -> ListF<S>, seed: S) -> FixList {
FixList(Box::new(coalg(seed).map(|s| ana(coalg, s))))
}Key Differences
ana and cata are mathematical duals — cata = fold (teardown), ana = unfold (buildup); this symmetry appears throughout category theory.Seq**: OCaml's Seq.unfold : ('b -> ('a * 'b) option) -> 'b -> 'a Seq.t is the standard anamorphism for lazy sequences — directly matching the coalgebra pattern.std::iter::successors(init, f) and unfold(init, f) (in itertools) implement the iterator version of anamorphisms.OCaml Approach
OCaml's anamorphism:
let rec ana coalg seed = Fix (map_list_f (ana coalg) (coalg seed))
(* Range example: *)
let range_coalg n = if n <= 0 then NilF else ConsF (n, n-1)
let range n = ana range_coalg n
OCaml's lazy sequences (Seq.t) provide the standard mechanism for anamorphism-style generation — each Seq.cons is one step of an anamorphism with the continuation as the tail thunk.
Full Source
#![allow(clippy::all)]
// Example 218: Anamorphism — Unfold to Build Recursive Structures
// ana : (seed -> F<seed>) -> seed -> Fix<F>
#[derive(Debug, Clone)]
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 map_ref<B>(&self, f: impl Fn(&A) -> B) -> ListF<B> {
match self {
ListF::NilF => ListF::NilF,
ListF::ConsF(x, a) => ListF::ConsF(*x, f(a)),
}
}
}
#[derive(Debug, Clone)]
struct FixList(Box<ListF<FixList>>);
fn ana<S>(coalg: &dyn Fn(S) -> ListF<S>, seed: S) -> FixList {
FixList(Box::new(coalg(seed).map(|s| ana(coalg, s))))
}
fn cata<A>(alg: &dyn Fn(ListF<A>) -> A, FixList(f): &FixList) -> A {
alg(f.map_ref(|child| cata(alg, child)))
}
fn to_vec(fl: &FixList) -> Vec<i64> {
cata(
&|l| match l {
ListF::NilF => vec![],
ListF::ConsF(x, mut acc) => {
acc.insert(0, x);
acc
}
},
fl,
)
}
// Approach 1: Range [lo..=hi]
fn range(lo: i64, hi: i64) -> FixList {
ana(
&|s: (i64, i64)| {
if s.0 > s.1 {
ListF::NilF
} else {
ListF::ConsF(s.0, (s.0 + 1, s.1))
}
},
(lo, hi),
)
}
// Approach 2: Countdown
fn countdown(n: i64) -> FixList {
ana(
&|s| {
if s <= 0 {
ListF::NilF
} else {
ListF::ConsF(s, s - 1)
}
},
n,
)
}
// Approach 3: Collatz sequence
fn collatz(n: i64) -> FixList {
ana(
&|s| {
if s <= 0 {
ListF::NilF
} else if s == 1 {
ListF::ConsF(1, 0)
} else if s % 2 == 0 {
ListF::ConsF(s, s / 2)
} else {
ListF::ConsF(s, 3 * s + 1)
}
},
n,
)
}
// Tree anamorphism
#[derive(Debug, Clone)]
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 map_ref<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)),
}
}
}
#[derive(Debug, Clone)]
struct FixTree(Box<TreeF<FixTree>>);
fn ana_tree<S>(coalg: &dyn Fn(S) -> TreeF<S>, seed: S) -> FixTree {
FixTree(Box::new(coalg(seed).map(|s| ana_tree(coalg, s))))
}
fn balanced_tree(depth: u32) -> FixTree {
ana_tree(
&|s: (u32, i64)| {
if s.0 == 0 {
TreeF::LeafF(s.1)
} else {
TreeF::BranchF((s.0 - 1, s.1), (s.0 - 1, s.1 + (1i64 << (s.0 - 1))))
}
},
(depth, 1),
)
}
fn tree_to_vec(t: &FixTree) -> Vec<i64> {
fn go(t: &FixTree) -> Vec<i64> {
match t.0.as_ref() {
TreeF::LeafF(n) => vec![*n],
TreeF::BranchF(l, r) => {
let mut v = go(l);
v.extend(go(r));
v
}
}
}
go(t)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_range() {
assert_eq!(to_vec(&range(1, 3)), vec![1, 2, 3]);
}
#[test]
fn test_countdown() {
assert_eq!(to_vec(&countdown(3)), vec![3, 2, 1]);
}
#[test]
fn test_collatz_1() {
assert_eq!(to_vec(&collatz(1)), vec![1]);
}
#[test]
fn test_balanced_tree() {
assert_eq!(tree_to_vec(&balanced_tree(1)), vec![1, 2]);
}
}
✓ Tests
Rust test suite
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_range() {
assert_eq!(to_vec(&range(1, 3)), vec![1, 2, 3]);
}
#[test]
fn test_countdown() {
assert_eq!(to_vec(&countdown(3)), vec![3, 2, 1]);
}
#[test]
fn test_collatz_1() {
assert_eq!(to_vec(&collatz(1)), vec![1]);
}
#[test]
fn test_balanced_tree() {
assert_eq!(tree_to_vec(&balanced_tree(1)), vec![1, 2]);
}
}
Deep Comparison
Comparison: Example 218 — Anamorphism
ana Definition
OCaml
let rec ana coalg seed =
FixL (map_lf (ana coalg) (coalg seed))
Rust
fn ana<S>(coalg: &dyn Fn(S) -> ListF<S>, seed: S) -> FixList {
FixList(Box::new(coalg(seed).map(|s| ana(coalg, s))))
}
Coalgebra: Range
OCaml
let range_coalg (lo, hi) =
if lo > hi then NilF
else ConsF (lo, (lo + 1, hi))
let range lo hi = ana range_coalg (lo, hi)
Rust
fn range(lo: i64, hi: i64) -> FixList {
ana(&|s: (i64, i64)| {
if s.0 > s.1 { ListF::NilF }
else { ListF::ConsF(s.0, (s.0 + 1, s.1)) }
}, (lo, hi))
}
Coalgebra: Collatz
OCaml
let collatz_coalg n =
if n <= 1 then ConsF (1, 0)
else if n mod 2 = 0 then ConsF (n, n / 2)
else ConsF (n, 3 * n + 1)
Rust
ana(&|s| {
if s <= 0 { ListF::NilF }
else if s == 1 { ListF::ConsF(1, 0) }
else if s % 2 == 0 { ListF::ConsF(s, s / 2) }
else { ListF::ConsF(s, 3 * s + 1) }
}, n)
Exercises
take_n(coalg, seed, n) that generates at most n elements — preventing infinite loops.ana to implement repeat(x) — an infinite list of the same value.