ExamplesBy LevelBy TopicLearning Paths
221 Expert

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

  • • Understand apomorphisms as anamorphisms with early termination (embedding existing structures)
  • • Learn how Either<Fix<F>, S> in the coalgebra return type enables short-circuiting
  • • See list insertion as the canonical example: insert an element, then embed the tail
  • • Understand the duality: apo is to ana as para is to cata
  • Code 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

  • Short-circuit power: apo terminates early by embedding existing structures; ana must unfold everything from scratch — apo is strictly more expressive.
  • Duality with para: apo : Seed -> Fix mirrors para : Fix -> Result dually; the "embed" vs. "expose" distinction is the symmetric difference.
  • Practical use: Sorted list insertion, search tree insertion with path copying, and "insert and replace" operations in functional structures are natural apomorphisms.
  • Either in coalgebra: The 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

  • Implement sorted insertion for a Vec<i64> using apo and verify the result is sorted.
  • Write apo_take(n, seed) that generates at most n elements from an infinite coalgebra by short-circuiting after n steps.
  • Implement replace_first(pred, new_val, list) using apo: replace the first element satisfying pred, then embed the rest unchanged.
  • Open Source Repos