Functor Category — Functors as Objects, Natural Transformations as Morphisms
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
Vec<T>, Option<T>) without higher-kinded types&[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
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.'a list -> 'a option and the polymorphism is implicit; Rust writes fn<T>(list: &[T]) -> Option<&T> with explicit generic parameters.list_to_option returns Option<&T> — a borrow into the slice — rather than cloning the first element, reflecting Rust's zero-copy preference.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));
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| Functor interface | module type FUNCTOR with type 'a t | No direct equivalent (no HKT in stable Rust) |
| List type | 'a list | &[T] (borrowed slice) |
| Natural transformation | 'a list -> 'a option | fn list_to_option<T>(list: &[T]) -> Option<&T> |
| Option type | 'a option | Option<T> |
| Map over list | List.map f lst | list.iter().cloned().map(f).collect::<Vec<_>>() |
| Map over option | Option.map f opt | opt.map(f) |
| Naturality assertion | assert (lhs = rhs) | naturality_holds(list, f) — returns bool |
Key Insights
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().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.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.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
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.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).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.