Category Basics
Functional Programming
Tutorial
The Problem
Category theory provides the mathematical foundation for functional programming abstractions — functors, monads, and natural transformations are all categorical concepts. A category consists of objects (types), morphisms (functions), composition (.), and identity (id). The category of types and functions (called Hask in Haskell, Typ in type theory) is the mathematical model that explains why Option, Result, and Vec all have map, and why and_then (bind) has its signature.
🎯 Learning Outcomes
Code Example
#![allow(clippy::all)]
// Example 226: Category Basics
// A category has objects, morphisms, composition, and identity.
// In Rust: types = objects, functions = morphisms.
// === Approach 1: Simple compose and identity functions ===
fn identity<A>(a: A) -> A {
a
}
fn compose<A, B, C>(f: impl Fn(B) -> C, g: impl Fn(A) -> B) -> impl Fn(A) -> C {
move |a| f(g(a))
}
// === Approach 2: Category trait (explicit abstraction) ===
trait Category {
type Obj;
// We represent morphisms as boxed closures for flexibility
fn id() -> Box<dyn Fn(Self::Obj) -> Self::Obj>;
fn compose(
f: Box<dyn Fn(Self::Obj) -> Self::Obj>,
g: Box<dyn Fn(Self::Obj) -> Self::Obj>,
) -> Box<dyn Fn(Self::Obj) -> Self::Obj>;
}
struct FnCategoryI32;
impl Category for FnCategoryI32 {
type Obj = i32;
fn id() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x)
}
fn compose(f: Box<dyn Fn(i32) -> i32>, g: Box<dyn Fn(i32) -> i32>) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| f(g(x)))
}
}
// === Approach 3: Kleisli category (a -> Option<b>) ===
fn kleisli_id<A>(a: A) -> Option<A> {
Some(a)
}
fn kleisli_compose<A, B, C>(
f: impl Fn(B) -> Option<C> + 'static,
g: impl Fn(A) -> Option<B> + 'static,
) -> impl Fn(A) -> Option<C> {
move |a| g(a).and_then(|b| f(b))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compose_basic() {
let f = |x: i32| x + 1;
let g = |x: i32| x * 2;
assert_eq!(compose(f, g)(5), 11);
}
#[test]
fn test_identity_laws() {
let f = |x: i32| x + 1;
assert_eq!(compose(identity, f)(10), f(10));
assert_eq!(compose(f, identity)(10), f(10));
}
#[test]
fn test_associativity() {
let f = |x: i32| x + 1;
let g = |x: i32| x * 2;
let h = |x: i32| x - 3;
assert_eq!(compose(compose(f, g), h)(7), compose(f, compose(g, h))(7));
}
#[test]
fn test_kleisli_compose() {
let safe_div = |x: i32| -> Option<i32> {
if x == 0 {
None
} else {
Some(100 / x)
}
};
let safe_succ = |x: i32| -> Option<i32> { Some(x + 1) };
let k = kleisli_compose(safe_succ, safe_div);
assert_eq!(k(5), Some(21));
assert_eq!(k(0), None);
}
}Key Differences
. for compose; OCaml uses >> or |>; Rust uses compose(f, g) — named function, no infix operator.compose and identity satisfy the laws by construction — they are mathematical truths, not runtime assertions.A -> Option<B> form a different category (Kleisli category for Option) where composition is and_then — this is the mathematical model for monadic composition.OCaml Approach
OCaml's standard library provides Fun.compose (since OCaml 4.08):
let (>>) f g x = g (f x)
let id x = x
OCaml's pipeline operator |> is related: x |> f = f x. Function composition is idiomatic OCaml; category theory is explicitly taught in the OCaml community through libraries like Base.Fn.compose.
Full Source
#![allow(clippy::all)]
// Example 226: Category Basics
// A category has objects, morphisms, composition, and identity.
// In Rust: types = objects, functions = morphisms.
// === Approach 1: Simple compose and identity functions ===
fn identity<A>(a: A) -> A {
a
}
fn compose<A, B, C>(f: impl Fn(B) -> C, g: impl Fn(A) -> B) -> impl Fn(A) -> C {
move |a| f(g(a))
}
// === Approach 2: Category trait (explicit abstraction) ===
trait Category {
type Obj;
// We represent morphisms as boxed closures for flexibility
fn id() -> Box<dyn Fn(Self::Obj) -> Self::Obj>;
fn compose(
f: Box<dyn Fn(Self::Obj) -> Self::Obj>,
g: Box<dyn Fn(Self::Obj) -> Self::Obj>,
) -> Box<dyn Fn(Self::Obj) -> Self::Obj>;
}
struct FnCategoryI32;
impl Category for FnCategoryI32 {
type Obj = i32;
fn id() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x)
}
fn compose(f: Box<dyn Fn(i32) -> i32>, g: Box<dyn Fn(i32) -> i32>) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| f(g(x)))
}
}
// === Approach 3: Kleisli category (a -> Option<b>) ===
fn kleisli_id<A>(a: A) -> Option<A> {
Some(a)
}
fn kleisli_compose<A, B, C>(
f: impl Fn(B) -> Option<C> + 'static,
g: impl Fn(A) -> Option<B> + 'static,
) -> impl Fn(A) -> Option<C> {
move |a| g(a).and_then(|b| f(b))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compose_basic() {
let f = |x: i32| x + 1;
let g = |x: i32| x * 2;
assert_eq!(compose(f, g)(5), 11);
}
#[test]
fn test_identity_laws() {
let f = |x: i32| x + 1;
assert_eq!(compose(identity, f)(10), f(10));
assert_eq!(compose(f, identity)(10), f(10));
}
#[test]
fn test_associativity() {
let f = |x: i32| x + 1;
let g = |x: i32| x * 2;
let h = |x: i32| x - 3;
assert_eq!(compose(compose(f, g), h)(7), compose(f, compose(g, h))(7));
}
#[test]
fn test_kleisli_compose() {
let safe_div = |x: i32| -> Option<i32> {
if x == 0 {
None
} else {
Some(100 / x)
}
};
let safe_succ = |x: i32| -> Option<i32> { Some(x + 1) };
let k = kleisli_compose(safe_succ, safe_div);
assert_eq!(k(5), Some(21));
assert_eq!(k(0), None);
}
}
✓ Tests
Rust test suite
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compose_basic() {
let f = |x: i32| x + 1;
let g = |x: i32| x * 2;
assert_eq!(compose(f, g)(5), 11);
}
#[test]
fn test_identity_laws() {
let f = |x: i32| x + 1;
assert_eq!(compose(identity, f)(10), f(10));
assert_eq!(compose(f, identity)(10), f(10));
}
#[test]
fn test_associativity() {
let f = |x: i32| x + 1;
let g = |x: i32| x * 2;
let h = |x: i32| x - 3;
assert_eq!(compose(compose(f, g), h)(7), compose(f, compose(g, h))(7));
}
#[test]
fn test_kleisli_compose() {
let safe_div = |x: i32| -> Option<i32> {
if x == 0 {
None
} else {
Some(100 / x)
}
};
let safe_succ = |x: i32| -> Option<i32> { Some(x + 1) };
let k = kleisli_compose(safe_succ, safe_div);
assert_eq!(k(5), Some(21));
assert_eq!(k(0), None);
}
}
Exercises
compose(f, identity) == f for a specific function f.compose(h, compose(g, f)) == compose(compose(h, g), f) for concrete f, g, h.kleisli_compose<A, B, C>(f: impl Fn(A) -> Option<B>, g: impl Fn(B) -> Option<C>) -> impl Fn(A) -> Option<C> and verify its laws.