Apomorphism — Ana that Can Short-Circuit
Functional Programming
Tutorial
The Problem
An anamorphism unfolds a seed step by step until the base case. But some unfolding algorithms know partway through that the rest of the structure is already built — they want to "embed" an existing structure rather than continuing to unfold. Apomorphisms extend anamorphisms with this short-circuit capability: the coalgebra can return either a new seed (continue unfolding) or an existing Fix<F> value (embed directly). This is the dual of a paramorphism.
🎯 Learning Outcomes
Either<Fix<F>, S> in the coalgebra return type enables short-circuitingapo is to ana as para is to cataCode Example
fn apo<S>(coalg: &dyn Fn(S) -> ListF<Either<FixList, S>>, seed: S) -> FixList {
FixList(Box::new(coalg(seed).map(|either| match either {
Either::Left(fix) => fix,
Either::Right(s) => apo(coalg, s),
})))
}Key Differences
apo terminates early by embedding existing structures; ana must unfold everything from scratch — apo is strictly more expressive.apo : Seed -> Fix mirrors para : Fix -> Result dually; the "embed" vs. "expose" distinction is the symmetric difference.Either<Fix<F>, S> return type is the key innovation; Right(seed) continues, Left(fix_value) short-circuits.OCaml Approach
OCaml's apomorphism:
let rec apo coalg seed =
Fix (map_list_f (function
| Left existing -> existing
| Right new_seed -> apo coalg new_seed)
(coalg seed))
The Either (called result or a custom type in OCaml) is the mechanism for short-circuiting. OCaml's pattern matching on Left/Right is direct and readable.
Full Source
#![allow(clippy::all)]
// Example 221: Apomorphism — Ana that Can Short-Circuit
#[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)),
}
}
}
#[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 to_vec(fl: &FixList) -> Vec<i64> {
let mut result = vec![];
let mut cur = fl;
loop {
match cur.0.as_ref() {
ListF::NilF => break,
ListF::ConsF(x, rest) => {
result.push(*x);
cur = rest;
}
}
}
result
}
// Either: Left = pre-built Fix (stop), Right = seed (continue)
enum Either<L, R> {
Left(L),
Right(R),
}
// apo: coalgebra returns F<Either<Fix, Seed>>
fn apo<S>(coalg: &dyn Fn(S) -> ListF<Either<FixList, S>>, seed: S) -> FixList {
FixList(Box::new(coalg(seed).map(|either| match either {
Either::Left(fix) => fix, // short-circuit
Either::Right(s) => apo(coalg, s), // continue
})))
}
// Approach 1: Insert into sorted list
fn insert(x: i64, lst: FixList) -> FixList {
apo(
&|fl: FixList| match fl.0.as_ref() {
ListF::NilF => ListF::ConsF(x, Either::Left(nil())),
ListF::ConsF(y, rest) => {
if x <= *y {
ListF::ConsF(x, Either::Left(fl.clone()))
} else {
ListF::ConsF(*y, Either::Right(rest.clone()))
}
}
},
lst,
)
}
// Approach 2: Take n elements
fn take(n: usize, lst: FixList) -> FixList {
apo(
&|s: (usize, FixList)| {
let (n, fl) = s;
if n == 0 {
return ListF::NilF;
}
match fl.0.as_ref() {
ListF::NilF => ListF::NilF,
ListF::ConsF(x, rest) => ListF::ConsF(*x, Either::Right((n - 1, rest.clone()))),
}
},
(n, lst),
)
}
// Approach 3: Replace first occurrence
fn replace_first(target: i64, replacement: i64, lst: FixList) -> FixList {
apo(
&|fl: FixList| match fl.0.as_ref() {
ListF::NilF => ListF::NilF,
ListF::ConsF(x, rest) => {
if *x == target {
ListF::ConsF(replacement, Either::Left(rest.clone()))
} else {
ListF::ConsF(*x, Either::Right(rest.clone()))
}
}
},
lst,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insert_middle() {
let l = cons(1, cons(3, nil()));
assert_eq!(to_vec(&insert(2, l)), vec![1, 2, 3]);
}
#[test]
fn test_take_empty() {
assert_eq!(to_vec(&take(5, nil())), vec![]);
}
#[test]
fn test_replace_not_found() {
let l = cons(1, cons(2, nil()));
assert_eq!(to_vec(&replace_first(99, 0, l)), vec![1, 2]);
}
}
✓ Tests
Rust test suite
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insert_middle() {
let l = cons(1, cons(3, nil()));
assert_eq!(to_vec(&insert(2, l)), vec![1, 2, 3]);
}
#[test]
fn test_take_empty() {
assert_eq!(to_vec(&take(5, nil())), vec![]);
}
#[test]
fn test_replace_not_found() {
let l = cons(1, cons(2, nil()));
assert_eq!(to_vec(&replace_first(99, 0, l)), vec![1, 2]);
}
}
Deep Comparison
Comparison: Example 221 — Apomorphism
apo Definition
OCaml
let rec apo coalg seed =
FixL (map_f (function
| Left fix -> fix
| Right s -> apo coalg s
) (coalg seed))
Rust
fn apo<S>(coalg: &dyn Fn(S) -> ListF<Either<FixList, S>>, seed: S) -> FixList {
FixList(Box::new(coalg(seed).map(|either| match either {
Either::Left(fix) => fix,
Either::Right(s) => apo(coalg, s),
})))
}
Insert into Sorted List
OCaml
let insert_coalg x = function
| FixL NilF -> ConsF (x, Left (FixL NilF))
| FixL (ConsF (y, rest)) as original ->
if x <= y then ConsF (x, Left original) (* short-circuit *)
else ConsF (y, Right rest) (* continue *)
Rust
apo(&|fl: FixList| match fl.0.as_ref() {
ListF::NilF => ListF::ConsF(x, Either::Left(nil())),
ListF::ConsF(y, rest) =>
if x <= *y { ListF::ConsF(x, Either::Left(fl.clone())) }
else { ListF::ConsF(*y, Either::Right(rest.clone())) },
}, lst)
Exercises
Vec<i64> using apo and verify the result is sorted.apo_take(n, seed) that generates at most n elements from an infinite coalgebra by short-circuiting after n steps.replace_first(pred, new_val, list) using apo: replace the first element satisfying pred, then embed the rest unchanged.