๐Ÿฆ€ Functional Rust

052: Functor Laws

Difficulty: โญโญ Intermediate Level: Intermediate Why `map` must follow two specific rules โ€” and what breaks when it doesn't.

The Problem This Solves

Suppose someone writes a "custom map" for their type. It looks right: it takes a function and returns a transformed container. You start using it in your code. Then things get weird.
let x = MyBox::new(42);
let y = x.map(|v| v);  // identity โ€” should be a no-op
assert_eq!(x, y);      // FAILS. Why?!
Or even stranger: you chain two maps, then try to replace them with a single composed map (a common optimization), and the results differ. Your code is full of "equivalent" expressions that aren't actually equivalent. Debugging becomes a nightmare because you can't trust what `map` does. This isn't hypothetical. Without laws, every type's `map` is a snowflake with hidden behavior. Code that looks like a refactoring is actually a behavior change. Generic algorithms that assume `map` is predictable silently produce wrong results. The Functor laws are two simple rules that tell you exactly what `map` is allowed to do โ€” and what it isn't. When every Functor follows these rules, you can reason about code, refactor safely, and trust that `map` never has secret side effects. This concept exists to solve exactly that pain.

The Intuition

Think of `map` like a vending machine that takes your snack request (a function) and applies it to what's inside without touching the machine's mechanics. Law 1 โ€” Identity: If you ask the machine to "give me exactly what's already there" (`|x| x`), you should get the exact same thing back. The machine should not have secretly counted your request, logged it, or changed the snack in any way. Law 2 โ€” Composition: If you first ask for "salted" then "crushed" (two separate passes), you should get the same result as if you had said "salted and crushed" in one request. Two maps in sequence should always equal one map with the composed function.
map(id) == identity              โ† Law 1: doing nothing does nothing
map(f).map(g) == map(gโˆ˜f)        โ† Law 2: two passes == one composed pass
These laws mean `map` is structure-preserving: it can change the values inside, but it cannot add elements, remove elements, reorder them, or count how many times it was called.

How It Works in Rust

Law 1 โ€” Identity:
fn verify_identity_law(x: Maybe<i32>) -> bool {
 x.clone().map(|v| v) == x  // map with identity function = original
}

assert!(verify_identity_law(Maybe::Just(42)));  // true for any value
assert!(verify_identity_law(Maybe::Nothing));   // true for empty too
Law 2 โ€” Composition:
fn verify_composition_law(f: fn(i32)->i32, g: fn(i32)->i32, x: Maybe<i32>) -> bool {
 let composed = x.clone().map(|v| f(g(v)));  // one map, composed function
 let chained  = x.map(g).map(f);             // two maps in sequence
 composed == chained
}
If both sides are equal for all functions `f`, `g` and all values `x`, your Functor is law-abiding. A law-breaking Functor โ€” the bad example:
struct BadFunctor<T> {
 value: T,
 map_count: usize,  // tracks how many times map was called
}

impl<T> BadFunctor<T> {
 fn map<U, F: FnOnce(T) -> U>(self, f: F) -> BadFunctor<U> {
     BadFunctor {
         value: f(self.value),
         map_count: self.map_count + 1,  // โ† breaks Law 1!
     }
 }
}
Why does this break Law 1? Because `bad.map(|x| x)` is not the same as `bad` โ€” the `map_count` changed from 0 to 1. The identity law is violated. It also breaks Law 2: `bad.map(g).map(f)` has `map_count = 2`, while `bad.map(|x| f(g(x)))` has `map_count = 1`. Two "equivalent" expressions give different results. Testing laws with standard Rust types:
// Vec follows both laws โ€” you can verify:
let xs = vec![1, 2, 3];
let after_id: Vec<i32> = xs.iter().copied().map(|x| x).collect();
assert_eq!(after_id, xs);  // Law 1: holds

let f = |x: i32| x * 2;
let g = |x: i32| x + 3;
let composed: Vec<i32> = xs.iter().copied().map(|x| f(g(x))).collect();
let chained:  Vec<i32> = xs.iter().copied().map(g).map(f).collect();
assert_eq!(composed, chained);  // Law 2: holds
`Option`, `Vec`, `Result` in Rust's standard library all satisfy both laws. That's why you can chain `.map()` calls freely and trust they compose.

What This Unlocks

Real codebases where Functor laws matter: `serde` (serialization must be structure-preserving), `rayon` (parallel map must compose like sequential map), iterator adapters in `std` (the entire adapter chain relies on composition law).

Key Differences

ConceptOCamlRust
Equality checkStructural `=` by default โ€” works on any typeRequires `#[derive(PartialEq)]` explicitly
Cloning for law testsNot needed โ€” immutable values can be shared/reusedMust call `.clone()` before testing to preserve the original
Law enforcementNot enforced by compiler โ€” convention and tests onlySame โ€” no compile-time enforcement; use `#[test]` to verify
Identity function`Fun.id` from stdlib, or `fun x -> x`Written inline as `\x\x` (no stdlib `id` function)
Composition`let compose f g x = f (g x)` โ€” explicit helperClosure composition: `\x\f(g(x))` inline
Bad functor detectionRuntime assertion: `assert (mapped <> original)``assert_ne!(mapped, original)` in `#[test]`
// Example 052: Functor Laws
// Law 1 (Identity): map(id, x) == x
// Law 2 (Composition): map(fโˆ˜g, x) == map(f, map(g, x))

#[derive(Debug, PartialEq, Clone)]
enum Maybe<T> {
    Nothing,
    Just(T),
}

impl<T> Maybe<T> {
    fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Maybe<U> {
        match self {
            Maybe::Nothing => Maybe::Nothing,
            Maybe::Just(x) => Maybe::Just(f(x)),
        }
    }
}

// Approach 1: Verify laws for Maybe
fn identity<T>(x: T) -> T { x }

fn verify_identity_law(x: Maybe<i32>) -> bool {
    x.clone().map(identity) == x
}

fn verify_composition_law(
    f: fn(i32) -> i32,
    g: fn(i32) -> i32,
    x: Maybe<i32>,
) -> bool {
    let composed = x.clone().map(|v| f(g(v)));
    let chained = x.map(g).map(f);
    composed == chained
}

// Approach 2: Verify laws for Vec (Rust's list)
fn vec_identity_law(xs: Vec<i32>) -> bool {
    let original = xs.clone();
    let mapped: Vec<i32> = xs.into_iter().map(identity).collect();
    mapped == original
}

fn vec_composition_law(f: fn(i32) -> i32, g: fn(i32) -> i32, xs: Vec<i32>) -> bool {
    let composed: Vec<i32> = xs.clone().into_iter().map(|x| f(g(x))).collect();
    let chained: Vec<i32> = xs.into_iter().map(g).map(f).collect();
    composed == chained
}

// Approach 3: Bad functor that breaks laws
#[derive(Debug, PartialEq, Clone)]
struct BadFunctor<T> {
    value: T,
    map_count: usize,
}

impl<T> BadFunctor<T> {
    fn new(value: T) -> Self {
        BadFunctor { value, map_count: 0 }
    }

    fn map<U, F: FnOnce(T) -> U>(self, f: F) -> BadFunctor<U> {
        BadFunctor {
            value: f(self.value),
            map_count: self.map_count + 1, // Breaks identity law!
        }
    }
}

fn main() {
    // Identity law
    println!("Identity law for Just(42): {}", verify_identity_law(Maybe::Just(42)));
    println!("Identity law for Nothing: {}", verify_identity_law(Maybe::Nothing));

    // Composition law
    let f: fn(i32) -> i32 = |x| x * 2;
    let g: fn(i32) -> i32 = |x| x + 3;
    println!("Composition law for Just(5): {}", verify_composition_law(f, g, Maybe::Just(5)));

    // Bad functor
    let bad = BadFunctor::new(42);
    let mapped = bad.clone().map(identity);
    println!("Bad functor identity law holds: {}", mapped == bad);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_identity_law_just() {
        assert!(verify_identity_law(Maybe::Just(42)));
    }

    #[test]
    fn test_identity_law_nothing() {
        assert!(verify_identity_law(Maybe::Nothing));
    }

    #[test]
    fn test_composition_law_just() {
        let f: fn(i32) -> i32 = |x| x * 2;
        let g: fn(i32) -> i32 = |x| x + 3;
        assert!(verify_composition_law(f, g, Maybe::Just(5)));
    }

    #[test]
    fn test_composition_law_nothing() {
        let f: fn(i32) -> i32 = |x| x * 2;
        let g: fn(i32) -> i32 = |x| x + 3;
        assert!(verify_composition_law(f, g, Maybe::Nothing));
    }

    #[test]
    fn test_vec_identity_law() {
        assert!(vec_identity_law(vec![1, 2, 3]));
        assert!(vec_identity_law(vec![]));
    }

    #[test]
    fn test_vec_composition_law() {
        let f: fn(i32) -> i32 = |x| x * 2;
        let g: fn(i32) -> i32 = |x| x + 3;
        assert!(vec_composition_law(f, g, vec![1, 2, 3]));
    }

    #[test]
    fn test_bad_functor_breaks_identity() {
        let bad = BadFunctor::new(42);
        let mapped = bad.clone().map(identity);
        assert_ne!(mapped, bad); // Identity law violated!
    }

    #[test]
    fn test_bad_functor_breaks_composition() {
        let f: fn(i32) -> i32 = |x| x * 2;
        let g: fn(i32) -> i32 = |x| x + 3;
        let bad = BadFunctor::new(5);
        let composed = bad.clone().map(|x| f(g(x)));
        let chained = bad.map(g).map(f);
        // map_count differs: composed=1, chained=2
        assert_ne!(composed, chained);
    }
}
(* Example 052: Functor Laws *)
(* Law 1 (Identity): map id x = x *)
(* Law 2 (Composition): map (f . g) x = map f (map g x) *)

type 'a maybe = Nothing | Just of 'a

let map f = function
  | Nothing -> Nothing
  | Just x -> Just (f x)

let id x = x
let compose f g x = f (g x)

(* Approach 1: Verify laws for Maybe *)
let verify_identity_law x =
  map id x = x

let verify_composition_law f g x =
  map (compose f g) x = map f (map g x)

(* Approach 2: Verify laws for List *)
let list_verify_identity xs =
  List.map id xs = xs

let list_verify_composition f g xs =
  List.map (compose f g) xs = List.map f (List.map g xs)

(* Approach 3: Counter-example โ€” a "bad functor" that breaks laws *)
module BadFunctor = struct
  type 'a t = Bad of 'a * int  (* tracks map count *)

  let map f (Bad (x, count)) = Bad (f x, count + 1)
  (* Breaks identity law: map id (Bad(x, 0)) = Bad(x, 1) โ‰  Bad(x, 0) *)
end

let () =
  (* Identity law for Maybe *)
  assert (verify_identity_law (Just 42));
  assert (verify_identity_law Nothing);

  (* Composition law for Maybe *)
  let f x = x * 2 in
  let g x = x + 3 in
  assert (verify_composition_law f g (Just 5));
  assert (verify_composition_law f g Nothing);

  (* Identity law for List *)
  assert (list_verify_identity [1; 2; 3]);
  assert (list_verify_identity []);

  (* Composition law for List *)
  assert (list_verify_composition f g [1; 2; 3]);

  (* Bad functor breaks identity *)
  let bad = BadFunctor.Bad (42, 0) in
  let mapped = BadFunctor.map id bad in
  assert (mapped <> bad);  (* Identity law violated! *)

  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Comparison: Functor Laws

Identity Law

OCaml:

๐Ÿช Show OCaml equivalent
let id x = x

(* map id x = x *)
assert (map id (Just 42) = Just 42);
assert (map id Nothing = Nothing);

Rust:

fn identity<T>(x: T) -> T { x }

// map(id, x) == x
assert_eq!(Maybe::Just(42).map(identity), Maybe::Just(42));
assert_eq!(Maybe::Nothing.map(identity), Maybe::Nothing);

Composition Law

OCaml:

๐Ÿช Show OCaml equivalent
let compose f g x = f (g x)
let f x = x * 2
let g x = x + 3

(* map (f . g) x = map f (map g x) *)
assert (map (compose f g) (Just 5) = map f (map g (Just 5)));

Rust:

let f = |x: i32| x * 2;
let g = |x: i32| x + 3;

// map(fโˆ˜g, x) == map(f, map(g, x))
let x = Maybe::Just(5);
assert_eq!(x.clone().map(|v| f(g(v))), x.map(g).map(f));

Bad Functor (Law Violation)

OCaml:

๐Ÿช Show OCaml equivalent
module BadFunctor = struct
type 'a t = Bad of 'a * int
let map f (Bad (x, count)) = Bad (f x, count + 1)
(* map id (Bad(x, 0)) = Bad(x, 1) โ‰  Bad(x, 0) *)
end

Rust:

struct BadFunctor<T> { value: T, map_count: usize }

impl<T> BadFunctor<T> {
 fn map<U, F: FnOnce(T) -> U>(self, f: F) -> BadFunctor<U> {
     BadFunctor { value: f(self.value), map_count: self.map_count + 1 }
     // Identity law violated: map_count changes!
 }
}