ExamplesBy LevelBy TopicLearning Paths
227 Advanced

Functor Category — Functors as Objects, Natural Transformations as Morphisms

Category Theory

Tutorial Video

Text description (accessibility)

This video demonstrates the "Functor Category — Functors as Objects, Natural Transformations as Morphisms" functional Rust example. Difficulty level: Advanced. Key concepts covered: Category Theory. In a functor category, objects are functors (type constructors with `map`) and morphisms are natural transformations (polymorphic functions between two functors that commute with `map`). Key difference from OCaml: 1. **Higher

Tutorial

The Problem

In a functor category, objects are functors (type constructors with map) and morphisms are natural transformations (polymorphic functions between two functors that commute with map). This example models two natural transformations — Vec → Option and Option → Vec — and verifies the naturality condition.

🎯 Learning Outcomes

  • • How Rust models functors through generic types (Vec<T>, Option<T>) without higher-kinded types
  • • How natural transformations become polymorphic generic functions in Rust
  • • The naturality condition expressed as a predicate over Rust functions
  • • Why slice borrows (&[T]) are idiomatic — no allocation, works on any contiguous sequence
  • 🦀 The Rust Way

    Rust lacks higher-kinded types, so Functor cannot be expressed as a trait parametrized over a type constructor. Instead, we work directly with concrete types (&[T], Option<T>, Vec<T>) and write generic functions that serve as natural transformations. The naturality condition becomes a generic predicate function that takes a list and a function and verifies commutativity.

    Code Example

    // Natural transformation: slice → Option, borrows in/out (no allocation)
    pub fn list_to_option<T>(list: &[T]) -> Option<&T> {
        list.first()
    }
    
    // Natural transformation: Option → Vec
    pub fn option_to_list<T>(opt: Option<T>) -> Vec<T> {
        match opt {
            None => vec![],
            Some(x) => vec![x],
        }
    }

    Key Differences

  • Higher-kinded types: OCaml can define module type FUNCTOR with type 'a t (an HKT); Rust cannot — there is no equivalent of F<T> as a type-level variable in stable Rust.
  • Natural transformations: OCaml writes 'a list -> 'a option and the polymorphism is implicit; Rust writes fn<T>(list: &[T]) -> Option<&T> with explicit generic parameters.
  • Ownership: list_to_option returns Option<&T> — a borrow into the slice — rather than cloning the first element, reflecting Rust's zero-copy preference.
  • Naturality verification: OCaml uses assert inline; Rust encodes it as a typed generic function naturality_holds<T, U, F> with T: Clone, U: PartialEq bounds, making the contract explicit.
  • OCaml Approach

    OCaml uses module signatures (FUNCTOR) to formally encode the functor interface, and polymorphic functions like 'a list -> 'a option naturally express natural transformations. The naturality condition is verified at runtime with assert. OCaml's first-class module system makes functor categories almost directly expressible.

    Full Source

    #![allow(clippy::all)]
    // In a functor category, objects are functors and morphisms are natural transformations.
    // Rust models functors as generic types with map-like operations (Vec, Option).
    // Natural transformations are polymorphic functions between two such types.
    
    // ---------------------------------------------------------------------------
    // Natural transformation: Vec → Option  (take the head element)
    // ---------------------------------------------------------------------------
    
    /// Solution 1: Idiomatic Rust — delegates to `slice::first`, borrows in/out.
    pub fn list_to_option<T>(list: &[T]) -> Option<&T> {
        list.first()
    }
    
    /// Solution 2: Functional/recursive style, mirroring OCaml pattern match.
    pub fn list_to_option_rec<T>(list: &[T]) -> Option<&T> {
        match list {
            [] => None,
            [head, ..] => Some(head),
        }
    }
    
    // ---------------------------------------------------------------------------
    // Natural transformation: Option → Vec  (wrap or empty)
    // ---------------------------------------------------------------------------
    
    /// Both idiomatic and functional: a single match is already the Rust idiom.
    pub fn option_to_list<T>(opt: Option<T>) -> Vec<T> {
        match opt {
            None => vec![],
            Some(x) => vec![x],
        }
    }
    
    // ---------------------------------------------------------------------------
    // Naturality condition
    //
    // A natural transformation η : F → G must commute with fmap:
    //   G.map(f) ∘ η  =  η ∘ F.map(f)
    //
    // Concretely for list_to_option:
    //   list_to_option(list.map(f))  ==  Option::map(f)(list_to_option(list))
    // ---------------------------------------------------------------------------
    
    /// Returns `true` when the naturality square commutes for `list_to_option` and `f`.
    pub fn naturality_holds<T, U, F>(list: &[T], f: F) -> bool
    where
        T: Clone,
        U: PartialEq,
        F: Fn(T) -> U,
    {
        // lhs: apply f to every element, then take the head
        let mapped: Vec<U> = list.iter().cloned().map(&f).collect();
        let lhs = mapped.first();
    
        // rhs: take the head first, then apply f
        let rhs = list.first().cloned().map(f);
    
        // compare Option<&U>  vs  Option<&U>  (rhs.as_ref() gives &U)
        lhs == rhs.as_ref()
    }
    
    // ---------------------------------------------------------------------------
    // Tests
    // ---------------------------------------------------------------------------
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_list_to_option_empty() {
            assert_eq!(list_to_option::<i32>(&[]), None);
        }
    
        #[test]
        fn test_list_to_option_single() {
            assert_eq!(list_to_option(&[42]), Some(&42));
        }
    
        #[test]
        fn test_list_to_option_multiple() {
            assert_eq!(list_to_option(&[1, 2, 3]), Some(&1));
        }
    
        #[test]
        fn test_list_to_option_rec_matches_idiomatic() {
            let cases: &[&[i32]] = &[&[], &[1], &[1, 2, 3]];
            for &list in cases {
                assert_eq!(list_to_option(list), list_to_option_rec(list));
            }
        }
    
        #[test]
        fn test_option_to_list_none() {
            assert_eq!(option_to_list::<i32>(None), vec![]);
        }
    
        #[test]
        fn test_option_to_list_some() {
            assert_eq!(option_to_list(Some(42)), vec![42]);
        }
    
        #[test]
        fn test_naturality_nonempty() {
            // list_to_option([2,4,6]) == map(*2)(list_to_option([1,2,3]))
            assert!(naturality_holds(&[1i32, 2, 3], |x| x * 2));
        }
    
        #[test]
        fn test_naturality_empty() {
            // Both sides are None — naturality trivially holds
            assert!(naturality_holds::<i32, i32, _>(&[], |x| x * 2));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_list_to_option_empty() {
            assert_eq!(list_to_option::<i32>(&[]), None);
        }
    
        #[test]
        fn test_list_to_option_single() {
            assert_eq!(list_to_option(&[42]), Some(&42));
        }
    
        #[test]
        fn test_list_to_option_multiple() {
            assert_eq!(list_to_option(&[1, 2, 3]), Some(&1));
        }
    
        #[test]
        fn test_list_to_option_rec_matches_idiomatic() {
            let cases: &[&[i32]] = &[&[], &[1], &[1, 2, 3]];
            for &list in cases {
                assert_eq!(list_to_option(list), list_to_option_rec(list));
            }
        }
    
        #[test]
        fn test_option_to_list_none() {
            assert_eq!(option_to_list::<i32>(None), vec![]);
        }
    
        #[test]
        fn test_option_to_list_some() {
            assert_eq!(option_to_list(Some(42)), vec![42]);
        }
    
        #[test]
        fn test_naturality_nonempty() {
            // list_to_option([2,4,6]) == map(*2)(list_to_option([1,2,3]))
            assert!(naturality_holds(&[1i32, 2, 3], |x| x * 2));
        }
    
        #[test]
        fn test_naturality_empty() {
            // Both sides are None — naturality trivially holds
            assert!(naturality_holds::<i32, i32, _>(&[], |x| x * 2));
        }
    }

    Deep Comparison

    OCaml vs Rust: Functor Category — Natural Transformations

    Side-by-Side Code

    OCaml

    (* Functor interface using module signature — HKT via type 'a t *)
    module type FUNCTOR = sig
      type 'a t
      val map : ('a -> 'b) -> 'a t -> 'b t
    end
    
    (* Natural transformation: list → option (take head) *)
    let list_to_option : 'a list -> 'a option = function
      | []     -> None
      | x :: _ -> Some x
    
    (* Naturality: map f . nat = nat . map f *)
    let check_naturality () =
      let f x = x * 2 in
      let lst = [1; 2; 3] in
      let lhs = list_to_option (List.map f lst) in
      let rhs = Option.map f (list_to_option lst) in
      assert (lhs = rhs)
    

    Rust (idiomatic)

    // Natural transformation: slice → Option, borrows in/out (no allocation)
    pub fn list_to_option<T>(list: &[T]) -> Option<&T> {
        list.first()
    }
    
    // Natural transformation: Option → Vec
    pub fn option_to_list<T>(opt: Option<T>) -> Vec<T> {
        match opt {
            None => vec![],
            Some(x) => vec![x],
        }
    }
    

    Rust (functional/recursive)

    // Same natural transformation, spelled out as a recursive pattern match
    // mirroring the OCaml `function | [] -> None | x :: _ -> Some x`
    pub fn list_to_option_rec<T>(list: &[T]) -> Option<&T> {
        match list {
            [] => None,
            [head, ..] => Some(head),
        }
    }
    
    // Naturality condition as a typed, generic predicate
    pub fn naturality_holds<T, U, F>(list: &[T], f: F) -> bool
    where
        T: Clone,
        U: PartialEq,
        F: Fn(T) -> U,
    {
        let mapped: Vec<U> = list.iter().cloned().map(&f).collect();
        let lhs = mapped.first();
        let rhs = list.first().cloned().map(f);
        lhs == rhs.as_ref()
    }
    

    Type Signatures

    ConceptOCamlRust
    Functor interfacemodule type FUNCTOR with type 'a tNo direct equivalent (no HKT in stable Rust)
    List type'a list&[T] (borrowed slice)
    Natural transformation'a list -> 'a optionfn list_to_option<T>(list: &[T]) -> Option<&T>
    Option type'a optionOption<T>
    Map over listList.map f lstlist.iter().cloned().map(f).collect::<Vec<_>>()
    Map over optionOption.map f optopt.map(f)
    Naturality assertionassert (lhs = rhs)naturality_holds(list, f) — returns bool

    Key Insights

  • HKT gap: OCaml can express FUNCTOR as a module type with type 'a t — a higher-kinded type. Rust has no equivalent in stable code, so the functor concept remains implicit in how Vec<T> and Option<T> both support .map().
  • Ownership shapes signatures: OCaml's list_to_option takes and returns values freely. In Rust, the idiomatic version takes a &[T] (a borrow) and returns Option<&T> — a reference into the input — avoiding any heap allocation for the common case.
  • Naturality as a first-class predicate: OCaml verifies naturality with an assert at runtime. Rust encodes it as naturality_holds<T, U, F> — a generic function whose type bounds (T: Clone, U: PartialEq, F: Fn(T) -> U) document exactly what the condition requires, making the proof obligation visible in the type system.
  • Pattern matching parity: The recursive Rust version (match list { [] => None, [head, ..] => Some(head) }) is almost a direct transliteration of OCaml's function | [] -> None | x :: _ -> Some x, showing how slice patterns bridge the two languages.
  • **as_ref() for cross-ownership comparison:** To compare Option<&U> (lhs) with Option<U> (rhs) in naturality_holds, Rust requires rhs.as_ref() to get Option<&U>. OCaml performs structural equality without worrying about ownership, illustrating how Rust's ownership model adds small but necessary boilerplate at comparison sites.
  • When to Use Each Style

    **Use idiomatic Rust (list.first())** when you need a fast, zero-copy check and the caller already holds the slice — the borrow is cheap and the API is maximally general.

    Use recursive Rust when teaching the OCaml parallel explicitly, or when the pattern-match structure itself is the point (e.g., demonstrating structural recursion or showing the empty/non-empty case split).

    Exercises

  • Implement a Compose<F, G> type that applies functor G inside functor F and implement fmap for it, demonstrating that the composition of two functors is a functor.
  • Write a natural transformation from Option<T> to Vec<T> (empty vec for None, singleton vec for Some) and verify it satisfies the naturality condition: fmap(f) . nat_transform == nat_transform . fmap(f).
  • Implement const_functor — a functor that ignores the fmap function and always returns the same wrapped value — and use it to count the number of elements in any functor context.
  • Open Source Repos