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
- Safe refactoring. You can replace `x.map(f).map(g)` with `x.map(|v| g(f(v)))` with confidence โ they're provably equal for any law-abiding Functor.
- Trustworthy generic code. Functions that accept any Functor can assume predictable behavior. Property-based testing tools (like `proptest`) can automatically verify these laws for your types.
- Catching subtle bugs early. A custom container with a broken `map` is a time bomb. Testing identity and composition laws catches it immediately, before it causes mysterious failures downstream.
Key Differences
| Concept | OCaml | Rust | ||
|---|---|---|---|---|
| Equality check | Structural `=` by default โ works on any type | Requires `#[derive(PartialEq)]` explicitly | ||
| Cloning for law tests | Not needed โ immutable values can be shared/reused | Must call `.clone()` before testing to preserve the original | ||
| Law enforcement | Not enforced by compiler โ convention and tests only | Same โ 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 helper | Closure composition: `\ | x\ | f(g(x))` inline |
| Bad functor detection | Runtime 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!
}
}