๐Ÿฆ€ Functional Rust

607: Fixed-Point Types and Recursion Schemes

Difficulty: 5 Level: Master Express recursive types as `Fix<F>` โ€” the fixed point of a functor โ€” to get catamorphism, anamorphism, and hylomorphism for free.

The Problem This Solves

Every time you define a recursive type (`List`, `Tree`, `Expr`), you end up writing the same fold and unfold patterns from scratch. A tree fold and a list fold look structurally identical โ€” but there's no abstraction that captures both without duplicating the recursion logic. The root cause is that recursive types bake recursion into their definition: `enum List<A> { Nil, Cons(A, Box<List<A>>) }`. What if you separated the "shape" of the type from the "recursion" itself? Define a non-recursive base functor `ListF<A, R>` where `R` is a placeholder for the recursive position. Then `Fix<ListF<A>>` is the recursive type. This separation is the key insight of recursion schemes: write your algebra once (`T โ†’ A`), apply `cata` to get a fold over any `Fix<F>` type. The recursive plumbing (traverse the structure, apply the algebra) is shared across all types. `Fix` is not academic โ€” it's the formal basis for `serde`, AST transformations, and any library that needs generic traversal.

The Intuition

`Fix<F>` is the type where `Fix<F> = F<Fix<F>>` โ€” it's the type that equals its own functor application, which is exactly what a recursive type is. The payoff: define your type as a non-recursive functor `F`, and you get `cata` (fold) and `ana` (unfold) for free, without re-implementing the recursion each time.

How It Works in Rust

// The fixed-point wrapper โ€” wraps one layer of F
struct Fix<F: Functor>(Box<F::Applied<Fix<F>>>);

// A trait for types that can map their recursive position
trait Functor {
 type Applied<T>;
 fn fmap<A, B>(fa: Self::Applied<A>, f: impl Fn(A) -> B) -> Self::Applied<B>;
}

// Base functor for List<A> โ€” R is the recursive position
enum ListF<A, R> {
 Nil,
 Cons(A, R),
}

// Manual Fixed-point for List โ€” simplified (Rust GATs make this easier in nightly)
enum ListFix<A> {
 Nil,
 Cons(A, Box<ListFix<A>>),  // in practice: Fix<ListF<A>>
}

impl<A> ListFix<A> {
 // Catamorphism: fold using an algebra (ListF<A, B> โ†’ B)
 fn cata<B>(self, alg: &impl Fn(Option<(A, B)>) -> B) -> B {
     match self {
         ListFix::Nil        => alg(None),
         ListFix::Cons(a, t) => {
             let tb = t.cata(alg);   // recurse first โ€” bottom-up
             alg(Some((a, tb)))       // then apply algebra
         }
     }
 }
}

// Using cata โ€” algebra defines the computation, recursion is handled by cata
let list = ListFix::Cons(1, Box::new(ListFix::Cons(2, Box::new(ListFix::Nil))));
let sum = list.cata(&|node| match node {
 None        => 0,
 Some((a, b)) => a + b,
});

What This Unlocks

Key Differences

ConceptOCamlRust
Fixed point`type 'f fix = Fix of 'f fix 'f``struct Fix<F>(Box<F::Applied<Fix<F>>>)`
Base functorType parameter in functor`trait Functor` with `type Applied<T>`
Recursive positionType variableGAT `Applied<T>` (stable since 1.65)
`cata``let rec cata alg = function ...`Method on `Fix<F>` or free function
Boxing overheadGC-managed`Box<T>` โ€” one heap alloc per node
Practical use`ppx_deriving` can generate foldsManual or via `recursion` crate
// Base functor for a simple Nat type
#[derive(Debug)]
enum NatF<A> { Zero, Succ(A) }

// Fix: wraps the recursive layer in a Box
struct Fix<F>(Box<F>);

type Nat = Fix<NatF<Fix<NatF<()>>>>;  // Only 2 levels for demo; true Fix requires GAT/recursive types

// More practical: define recursion schemes over concrete types

// Expression base functor (non-recursive)
#[derive(Debug,Clone)]
enum ExprF<R> {
    Lit(i64),
    Add(R, R),
    Mul(R, R),
    Neg(R),
}

// Recursive expression using Fix-like structure
#[derive(Debug,Clone)]
struct Expr(Box<ExprF<Expr>>);

impl Expr {
    fn lit(n: i64) -> Self { Expr(Box::new(ExprF::Lit(n))) }
    fn add(l: Self, r: Self) -> Self { Expr(Box::new(ExprF::Add(l, r))) }
    fn mul(l: Self, r: Self) -> Self { Expr(Box::new(ExprF::Mul(l, r))) }
    fn neg(x: Self) -> Self { Expr(Box::new(ExprF::Neg(x))) }
}

// Catamorphism (fold) over the expression
fn cata<R>(expr: Expr, alg: &impl Fn(ExprF<R>) -> R) -> R {
    match *expr.0 {
        ExprF::Lit(n)    => alg(ExprF::Lit(n)),
        ExprF::Add(l,r)  => { let l=cata(l,alg); let r=cata(r,alg); alg(ExprF::Add(l,r)) }
        ExprF::Mul(l,r)  => { let l=cata(l,alg); let r=cata(r,alg); alg(ExprF::Mul(l,r)) }
        ExprF::Neg(x)    => { let x=cata(x,alg); alg(ExprF::Neg(x)) }
    }
}

// Two algebras
fn eval_alg(e: ExprF<i64>) -> i64 {
    match e { ExprF::Lit(n)=>n, ExprF::Add(l,r)=>l+r, ExprF::Mul(l,r)=>l*r, ExprF::Neg(x)=>-x }
}

fn print_alg(e: ExprF<String>) -> String {
    match e {
        ExprF::Lit(n)    => format!("{}", n),
        ExprF::Add(l,r)  => format!("({}+{})",l,r),
        ExprF::Mul(l,r)  => format!("({}*{})",l,r),
        ExprF::Neg(x)    => format!("(-{})",x),
    }
}

fn main() {
    // (3*4) + (-2)
    let e = Expr::add(
        Expr::mul(Expr::lit(3), Expr::lit(4)),
        Expr::neg(Expr::lit(2)),
    );
    let e2 = e.clone();
    println!("eval  = {}", cata(e,  &eval_alg));
    println!("print = {}", cata(e2, &print_alg));
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test] fn eval_lit() { let e=Expr::lit(42); assert_eq!(cata(e,&eval_alg),42); }
    #[test] fn eval_add() { let e=Expr::add(Expr::lit(2),Expr::lit(3)); assert_eq!(cata(e,&eval_alg),5); }
}
(* Fixed-point types in OCaml *)

(* Fix-point of a functor *)
type 'f fix = Fix of ('f fix) 'f [@@unboxed]

(* Base functor for Nat *)
type 'a nat_f = Zero | Succ of 'a

(* Fix Nat *)
type nat = nat_f fix

let zero     = Fix Zero
let succ n   = Fix (Succ n)

let to_int (Fix n) =
  let rec go acc = function
    | Zero   -> acc
    | Succ (Fix n) -> go (acc+1) n
  in go 0 n

let () =
  let three = succ (succ (succ zero)) in
  Printf.printf "three = %d\n" (to_int three)