Paramorphism — Cata with Access to Original Subtree
Functional Programming
Tutorial
The Problem
A catamorphism's algebra sees only the folded results of children, not the original sub-structure. But some algorithms need both — the original subtree and its folded result simultaneously. Factorial needs "the predecessor" and its result; a tree algorithm may need the original child count and the accumulated value. Paramorphisms extend catamorphisms: the algebra receives pairs of (original_subtree, accumulated_result) for each child.
🎯 Learning Outcomes
para function: algebra receives F<(Fix<F>, A)> instead of F<A>para is needed vs. cata: when the original structure matters, not just the resultCode Example
fn para<A: Clone>(alg: &dyn Fn(ListF<(A, FixList)>) -> A, fl: &FixList) -> A {
let paired = fl.0.map_ref(|child| (para(alg, child), child.clone()));
alg(paired)
}Key Differences
para preserves the original structure at each step; cata discards it after folding — this is the essential difference.cata can be expressed as a para (ignore the original structure parameter); para is strictly more expressive.(Fix<F>, A) pairs require allocating tuples at each node; this is the cost of the extra information.OCaml Approach
OCaml's paramorphism:
let rec para alg (Fix lf) =
alg (map_list_f (fun child -> (child, para alg child)) lf)
Each child is paired with its recursed result. OCaml's let rec makes the mutual structure clear. The list tail in a para computation is available as the original FixList value, not just the folded integer result.
Full Source
#![allow(clippy::all)]
// Example 220: Paramorphism — Cata with Access to Original Subtree
#[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 nil() -> FixList {
FixList(Box::new(ListF::NilF))
}
fn cons(x: i64, xs: FixList) -> FixList {
FixList(Box::new(ListF::ConsF(x, xs)))
}
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,
)
}
// para: algebra gets (result, original_subtree) for each child
fn para<A: Clone>(alg: &dyn Fn(ListF<(A, FixList)>) -> A, fl: &FixList) -> A {
let paired = fl.0.map_ref(|child| (para(alg, child), child.clone()));
alg(paired)
}
// Approach 1: tails — needs original subtree to convert
fn tails(fl: &FixList) -> Vec<Vec<i64>> {
// Simple recursive implementation using para
// tails [1,2] = [[1,2], [2], []]
let full = to_vec(fl);
let mut result = vec![full];
let sub_tails = para(
&|l: ListF<(Vec<Vec<i64>>, FixList)>| match l {
ListF::NilF => vec![],
ListF::ConsF(_, (rest_tails, original_tail)) => {
let mut v = vec![to_vec(&original_tail)];
v.extend(rest_tails);
v
}
},
fl,
);
result.extend(sub_tails);
result
}
// Approach 2: Sliding window
fn sliding_window(n: usize, fl: &FixList) -> Vec<Vec<i64>> {
para(
&|l: ListF<(Vec<Vec<i64>>, FixList)>| match l {
ListF::NilF => vec![],
ListF::ConsF(x, (rest_windows, original_tail)) => {
let mut remainder = vec![x];
remainder.extend(to_vec(&original_tail));
let mut result = if remainder.len() >= n {
vec![remainder[..n].to_vec()]
} else {
vec![]
};
result.extend(rest_windows);
result
}
},
fl,
)
}
// Approach 3: drop_while
fn drop_while(pred: impl Fn(i64) -> bool, fl: &FixList) -> Vec<i64> {
para(
&|l: ListF<(Vec<i64>, FixList)>| match l {
ListF::NilF => vec![],
ListF::ConsF(x, (rest, original_tail)) => {
if pred(x) {
rest
} else {
let mut v = vec![x];
v.extend(to_vec(&original_tail));
v
}
}
},
fl,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tails() {
let xs = cons(1, cons(2, nil()));
assert_eq!(tails(&xs), vec![vec![1, 2], vec![2], vec![]]);
}
#[test]
fn test_sliding() {
let xs = cons(1, cons(2, cons(3, cons(4, nil()))));
assert_eq!(sliding_window(3, &xs), vec![vec![1, 2, 3], vec![2, 3, 4]]);
}
#[test]
fn test_drop_while_all() {
let xs = cons(1, cons(2, nil()));
assert_eq!(drop_while(|_| true, &xs), vec![]);
}
}
✓ Tests
Rust test suite
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tails() {
let xs = cons(1, cons(2, nil()));
assert_eq!(tails(&xs), vec![vec![1, 2], vec![2], vec![]]);
}
#[test]
fn test_sliding() {
let xs = cons(1, cons(2, cons(3, cons(4, nil()))));
assert_eq!(sliding_window(3, &xs), vec![vec![1, 2, 3], vec![2, 3, 4]]);
}
#[test]
fn test_drop_while_all() {
let xs = cons(1, cons(2, nil()));
assert_eq!(drop_while(|_| true, &xs), vec![]);
}
}
Deep Comparison
Comparison: Example 220 — Paramorphism
para Definition
OCaml
let rec para alg (FixL f as original) =
let paired = map_f (fun child -> (para alg child, child)) f in
alg paired
Rust
fn para<A: Clone>(alg: &dyn Fn(ListF<(A, FixList)>) -> A, fl: &FixList) -> A {
let paired = fl.0.map_ref(|child| (para(alg, child), child.clone()));
alg(paired)
}
tails Algebra
OCaml
let tails_alg = function
| NilF -> [[]]
| ConsF (_, (rest_tails, original_tail)) ->
to_list original_tail :: rest_tails
Rust
ListF::NilF => vec![vec![]],
ListF::ConsF(_, (rest_tails, original_tail)) => {
let mut v = vec![to_vec(&original_tail)];
v.extend(rest_tails);
v
}
Exercises
list_suffixes using para: for [1, 2, 3] return [[1, 2, 3], [2, 3], [3], []] — each suffix is the original tail.sliding_sum(n) that sums every window of n consecutive elements using para.indexed_fold that pairs each element with its index using para.