ExamplesBy LevelBy TopicLearning Paths
612 Expert

Zygomorphism

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Zygomorphism" functional Rust example. Difficulty level: Expert. Key concepts covered: Functional Programming. A zygomorphism (zygo) is a mutually recursive fold where two computations run simultaneously, one depending on the other. Key difference from OCaml: 1. **HKT requirement**: These morphisms ideally require higher

Tutorial

The Problem

A zygomorphism (zygo) is a mutually recursive fold where two computations run simultaneously, one depending on the other. The classic example: compute both the value and its depth simultaneously in a single tree traversal. Zygomorphisms generalize paramorphisms and are more efficient than running two separate traversals.

🎯 Learning Outcomes

  • • The categorical definition and the mathematical laws that must hold
  • • How to implement this pattern in Rust despite the lack of higher-kinded types
  • • The relationship to more familiar functional idioms (fold, unfold, map)
  • • Key concepts: zygo, mutually recursive fold, paired accumulation
  • • Where this pattern appears in production systems and when simpler alternatives suffice
  • Code Example

    #![allow(clippy::all)]
    //! # Zygomorphism
    //! Two folds running in parallel.
    
    pub fn zygo<A, B, C>(
        xs: &[A],
        nil_b: B,
        cons_b: impl Fn(&A, &B) -> B,
        nil_c: C,
        cons_c: impl Fn(&A, &B, C) -> C,
    ) -> C
    where
        B: Clone,
    {
        let mut bs: Vec<B> = vec![nil_b.clone()];
        for x in xs.iter().rev() {
            bs.push(cons_b(x, bs.last().unwrap()));
        }
        bs.reverse();
        let mut c = nil_c;
        for (i, x) in xs.iter().enumerate().rev() {
            c = cons_c(x, &bs[i + 1], c);
        }
        c
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_zygo() {
            let sum = zygo(&[1, 2, 3], 0, |x, b| x + b, 0, |_, _, c| c + 1);
            assert_eq!(sum, 3);
        }
    }

    Key Differences

  • HKT requirement: These morphisms ideally require higher-kinded types for full generality; Rust uses GATs or associated types as approximations.
  • Performance: Rust's implementations are more verbose but compile to efficient machine code; OCaml's implementations are more concise with similar runtime performance.
  • Practical adoption: In Haskell, recursive schemes from recursion-schemes are widely used; in Rust and OCaml, direct recursion is more common in practice.
  • Theoretical value: Understanding these patterns deepens intuition for all recursive programming, even when direct recursion is used in production code.
  • OCaml Approach

    OCaml's pattern matching and recursive types make morphism implementations natural. The Fix type and F-algebra/coalgebra patterns translate directly, though without Haskell's typeclass machinery:

    (* OCaml recursive schemes use:
       - Recursive variant types for F-algebras
       - Higher-order functions for the morphism
       - GADTs for type-safe fixed points in advanced cases *)
    

    Full Source

    #![allow(clippy::all)]
    //! # Zygomorphism
    //! Two folds running in parallel.
    
    pub fn zygo<A, B, C>(
        xs: &[A],
        nil_b: B,
        cons_b: impl Fn(&A, &B) -> B,
        nil_c: C,
        cons_c: impl Fn(&A, &B, C) -> C,
    ) -> C
    where
        B: Clone,
    {
        let mut bs: Vec<B> = vec![nil_b.clone()];
        for x in xs.iter().rev() {
            bs.push(cons_b(x, bs.last().unwrap()));
        }
        bs.reverse();
        let mut c = nil_c;
        for (i, x) in xs.iter().enumerate().rev() {
            c = cons_c(x, &bs[i + 1], c);
        }
        c
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_zygo() {
            let sum = zygo(&[1, 2, 3], 0, |x, b| x + b, 0, |_, _, c| c + 1);
            assert_eq!(sum, 3);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_zygo() {
            let sum = zygo(&[1, 2, 3], 0, |x, b| x + b, 0, |_, _, c| c + 1);
            assert_eq!(sum, 3);
        }
    }

    Deep Comparison

    Zygomorphism

    Two folds with one depending on the other

    Exercises

  • Laws verification: Write tests that verify the categorical laws for this morphism on a specific data type.
  • New data type: Apply the morphism to a different recursive data type (e.g., apply catamorphism to a rose tree instead of a binary tree).
  • Comparison: Implement the same computation using direct recursion and the morphism — measure whether the morphism version composes more cleanly.
  • Open Source Repos