๐Ÿฆ€ Functional Rust

246: Adjunctions

Difficulty: 5 Level: Master Two operations are "adjoint" when converting in one direction is always equivalent to converting in the other โ€” the most elegant way to explain why currying, monads, and duality work the way they do.

The Problem This Solves

Why does `curry` undo `uncurry` and vice versa? Why does the `Vec` monad's `flat_map` feel like a "natural" generalization of `map`? Why do `State` and `Reader` monads feel like mirror images? Why does the list monad arise from "free" and "forgetful" functors? These feel like unrelated coincidences until you see the pattern underneath: adjunctions. Every monad arises from an adjunction. Currying is an adjunction. Free structures (like `Vec`) are always adjoint to forgetful functors (like "just the set of elements"). Understanding adjunctions replaces a dozen separate explanations with one. Without this concept, you end up with a zoo of "why does this particular pattern work?" questions answered case-by-case. With adjunctions, you see they're all the same thing viewed from different angles. The pattern is: two operations `F` and `G` are adjoint when there's a perfect, invertible correspondence between "inputs to F's output" and "inputs to G's output." This exists to solve exactly that pain.

The Intuition

Two functions `F` and `G` are adjoint (written F โŠฃ G) when there's a natural correspondence: > A function from `F(A)` to `B` โ‰… A function from `A` to `G(B)` Read that again: any way to get from `F(A)` to `B` corresponds to exactly one way to get from `A` to `G(B)`, and vice versa. This is an isomorphism โ€” both sides have the same information, just reorganized. The most concrete example you already know: currying.
curry   :: (A, C) -> B    โ‰…    A -> (C -> B)
uncurry :: A -> (C -> B)  โ‰…    (A, C) -> B
Here `F(A) = (A, C)` (pairing with `C`) and `G(B) = C -> B` (functions from `C`). A function that takes a pair and returns `B` is equivalent to a function that takes `A` and returns a function `C -> B`. They carry identical information. `curry` and `uncurry` are the isomorphism. This is the Product โŠฃ Exponential adjunction. The "pairing with C" functor is adjoint to the "functions from C" functor. That's all currying is. The unit and counit: Every adjunction has two natural transformations: They satisfy triangle identities: applying unit then counit gets you back. This is what makes `curry . uncurry = id` and `uncurry . curry = id`. How monads arise: The composition `G โˆ˜ F` is always a monad! For currying: `G(F(A)) = C -> (A, C)` โ€” that's the State monad. Every monad comes from some adjunction. This is why `Vec` (the free monoid) is a monad: Free โŠฃ Forgetful gives you the list monad.

How It Works in Rust

// The Product โŠฃ Exponential adjunction = curry/uncurry
// curry: Hom(A ร— C, B) -> Hom(A, C -> B)
pub fn curry<A: Clone + 'static, B: 'static, C: 'static>(
 f: impl Fn(A, C) -> B + 'static,
) -> impl Fn(A) -> Box<dyn Fn(C) -> B> {
 let f = Rc::new(f);
 move |a: A| {
     let a2 = a.clone();
     let f2 = f.clone();
     // Return a closure that captures 'a' โ€” this IS the adjunction
     Box::new(move |c: C| f2(a2.clone(), c))
 }
}

// uncurry: Hom(A, C -> B) -> Hom(A ร— C, B)  (the inverse)
pub fn uncurry<A: Clone + 'static, B: 'static, C: 'static>(
 f: impl Fn(A) -> Box<dyn Fn(C) -> B> + 'static,
) -> impl Fn(A, C) -> B {
 move |a, c| (f(a))(c)  // apply then apply โ€” the counit
}

// Verify the adjunction isomorphism holds:
let add = |a: i32, b: i32| a + b;
let add_rt = uncurry(curry(add));
assert_eq!(add_rt(3, 4), 7);  // uncurry(curry(f)) = f โœ“

// The Free โŠฃ Forgetful adjunction = list monad
// Unit of the adjunction: a -> [a]  (inject into free structure)
pub fn free_unit<A>(a: A) -> Vec<A> { vec![a] }

// Counit: Vec<Vec<A>> -> Vec<A>  (flatten = fold the free structure)
pub fn free_counit<A>(vv: Vec<Vec<A>>) -> Vec<A> {
 vv.into_iter().flatten().collect()
}

// The monad arising from Free โŠฃ Forgetful:
// return = free_unit, bind = flat_map
pub fn list_bind<A: Clone, B>(xs: Vec<A>, f: impl Fn(A) -> Vec<B>) -> Vec<B> {
 xs.into_iter().flat_map(f).collect()
}

// Option monad โ€” also arises from an adjunction
// return = Some, join = flatten
pub fn option_join<A>(oa: Option<Option<A>>) -> Option<A> { oa.flatten() }
The key insight in `curry`: the closure `move |c| f(a, c)` is exactly the adjunction at work โ€” it "stores" `a` and waits for `c`. The adjoint "reorganizes" where the arguments live.

What This Unlocks

Key Differences

ConceptOCamlRust
`curry` type`('a * 'c -> 'b) -> 'a -> 'c -> 'b``impl Fn(A, C) -> B` โ†’ `impl Fn(A) -> Box<dyn Fn(C) -> B>`
Returning closuresFirst-class, no boxing neededMust box (`Box<dyn Fn(C) -> B>`) due to size requirements
`Rc` for sharingNot needed (GC handles it)`Rc::new(f)` to share `f` across multiple closures
Unit/counitNatural transformations between polymorphic typesConcrete functions; no higher-kinded types
Triangle identitiesEquational proofsTested via `assert_eq!(uncurry(curry(f))(a, b), f(a, b))`
Monad derivation`module StateMonad = Compose(G)(F)`Demonstrated separately (see example 249 for State monad)
/// Adjunctions: F โŠฃ G
///
/// F and G are adjoint if there is a natural isomorphism:
///   Hom(F A, B) โ‰… Hom(A, G B)
///
/// Equivalently, there are natural transformations:
///   unit   :: A -> G(F(A))       (also called ฮท)
///   counit :: F(G(B)) -> B       (also called ฮต)
///
/// satisfying the triangle identities:
///   counit(F(unit(a))) = a   (or in Rust: counit . F(unit) = id)
///   G(counit(unit(g))) = g
///
/// โ”€โ”€ The canonical example: Product โŠฃ Exponential โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
///
///   F(A) = A ร— C   (product with C on the right)
///   G(B) = C โ†’ B   (exponential / function type)
///
/// Hom(A ร— C, B) โ‰… Hom(A, C โ†’ B)
/// This is just curry/uncurry!
///
/// โ”€โ”€ Monad from adjunction โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
/// G โˆ˜ F is a monad with:
///   return = unit
///   join   = G(counit . F)  (which becomes State monad for this adjunction)

// โ”€โ”€ Curry/Uncurry Adjunction โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

use std::rc::Rc;

/// curry: (A ร— C -> B) -> (A -> C -> B)
/// This is the isomorphism Hom(F A, B) -> Hom(A, G B)
pub fn curry<A: Clone + 'static, B: 'static, C: 'static>(
    f: impl Fn(A, C) -> B + 'static,
) -> impl Fn(A) -> Box<dyn Fn(C) -> B> {
    let f = Rc::new(f);
    move |a: A| {
        let a2 = a.clone();
        let f2 = f.clone();
        Box::new(move |c: C| f2(a2.clone(), c))
    }
}

/// uncurry: (A -> C -> B) -> (A ร— C -> B)
/// This is the inverse isomorphism Hom(A, G B) -> Hom(F A, B)
pub fn uncurry<A: Clone + 'static, B: 'static, C: 'static>(
    f: impl Fn(A) -> Box<dyn Fn(C) -> B> + 'static,
) -> impl Fn(A, C) -> B {
    move |a, c| (f(a))(c)
}

/// unit of the adjunction: a -> C -> (A ร— C)
/// unit_adj c a = (a, c) โ€” pair a with the fixed context c
pub fn unit_adj<A: Clone, C: Clone>(c: C) -> impl Fn(A) -> (A, C) {
    move |a| (a, c.clone())
}

/// counit of the adjunction: (A ร— C -> B) ร— (A ร— C) -> B
/// = function application
pub fn counit_adj<A, B, C>(f: impl Fn(A, C) -> B, a: A, c: C) -> B {
    f(a, c)
}

// โ”€โ”€ List Adjunction: Free โŠฃ Forgetful โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
///
/// The free monoid adjunction:
///   F(A) = Vec<A>   (free monoid on A)
///   G(M) = underlying set of M (forgetful functor)
///
/// Hom_Mon(Vec<A>, M) โ‰… Hom_Set(A, G M)
/// A monoid homomorphism from Vec<A> is determined by where it sends generators.

/// Unit of the Free โŠฃ Forgetful adjunction: a -> [a]
pub fn free_unit<A>(a: A) -> Vec<A> {
    vec![a]
}

/// Counit: fold a Vec<Vec<A>> (free monad structure) into Vec<A>
/// This is Vec::concat (flatten)
pub fn free_counit<A>(vv: Vec<Vec<A>>) -> Vec<A> {
    vv.into_iter().flatten().collect()
}

/// The monad arising from the Free-Forgetful adjunction is Vec (list monad).
/// return = free_unit, bind = flat_map
pub fn list_bind<A: Clone, B>(xs: Vec<A>, f: impl Fn(A) -> Vec<B>) -> Vec<B> {
    xs.into_iter().flat_map(f).collect()
}

// โ”€โ”€ Option Adjunction โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
///
/// Option โŠฃ Diagonal (in some sense):
/// More precisely, the adjunction between pointed sets and sets.
/// Return = Some, join = Option::flatten

pub fn option_return<A>(a: A) -> Option<A> {
    Some(a)
}

pub fn option_join<A>(oa: Option<Option<A>>) -> Option<A> {
    oa.flatten()
}

// โ”€โ”€ Verify the triangle identities โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

fn main() {
    println!("=== Adjunctions ===\n");
    println!("F โŠฃ G means: Hom(F A, B) โ‰… Hom(A, G B)");
    println!("Unit:   ฮท :: A -> G(F(A))");
    println!("Counit: ฮต :: F(G(B)) -> B\n");

    println!("โ”€โ”€ Product โŠฃ Exponential (curry/uncurry) โ”€โ”€\n");

    let add = |a: i32, b: i32| a + b;
    let curried_add = curry(add);
    println!("curry(add)(3)(4) = {}", (curried_add(3))(4));

    // uncurry expects fn that returns Box<dyn Fn>
    let mul_boxed = |a: i32| -> Box<dyn Fn(i32) -> i32> { Box::new(move |b: i32| a * b) };
    let uncurried_mul = uncurry(mul_boxed);
    println!("uncurry(mul)(3, 4) = {}", uncurried_mul(3, 4));

    // Verify: curry . uncurry = id
    let add2 = |a: i32, b: i32| a + b;
    let add2_rt = uncurry(curry(add2));
    assert_eq!(add2_rt(3, 4), 7);
    println!("\nuncurry(curry(add))(3,4) = {} โœ“", add2_rt(3, 4));

    // Verify: uncurry . curry = id
    let m1 = 5_i32 * 6;
    let mul_boxed2 = |a: i32| -> Box<dyn Fn(i32) -> i32> { Box::new(move |b: i32| a * b) };
    let m2 = (curry(uncurry(mul_boxed2))(5))(6);
    assert_eq!(m1, m2);
    println!("curry(uncurry(mul))(5)(6) = {} โœ“", m2);

    // Unit and counit
    println!("\nUnit (pair with context 10): {:?}", unit_adj(10_i32)(42_i32));
    let f = |a: i32, c: i32| a + c;
    println!("Counit (apply): f(3, 10) = {}", counit_adj(f, 3, 10));

    println!("\nโ”€โ”€ Free โŠฃ Forgetful (Vec / list monad) โ”€โ”€\n");

    // Free unit: a -> [a]
    println!("free_unit(42) = {:?}", free_unit(42_i32));

    // Counit: flatten
    println!("free_counit([[1,2],[3],[4,5]]) = {:?}",
        free_counit(vec![vec![1, 2], vec![3], vec![4, 5]]));

    // List monad from adjunction
    let result = list_bind(vec![1_i32, 2, 3], |x| vec![x, x * 10]);
    println!("list_bind [1,2,3] (\\x -> [x, x*10]) = {:?}", result);

    println!("\nโ”€โ”€ Option monad from adjunction โ”€โ”€\n");
    println!("option_return(5) = {:?}", option_return(5_i32));
    println!("option_join(Some(Some(3))) = {:?}", option_join(Some(Some(3_i32))));
    println!("option_join(Some(None::<i32>)) = {:?}", option_join(Some(None::<i32>)));
    println!("option_join(None::<Option<i32>>) = {:?}", option_join(None::<Option<i32>>));

    println!();
    println!("Key theorem: every adjunction F โŠฃ G yields a monad Gโˆ˜F.");
    println!("Product โŠฃ Exponential โ†’ State monad (see example 249).");
    println!("Free โŠฃ Forgetful โ†’ List monad.");
    println!("Maybe โŠฃ Pointed Set โ†’ Option monad.");
}

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

    #[test]
    fn test_curry_uncurry_roundtrip() {
        let f = |a: i32, b: i32| a + b;
        let g = uncurry(curry(f));
        assert_eq!(g(3, 4), 7);
        assert_eq!(g(0, 0), 0);
    }

    #[test]
    fn test_uncurry_curry_roundtrip() {
        let f = |a: i32| -> Box<dyn Fn(i32) -> i32> { Box::new(move |b: i32| a * b) };
        assert_eq!((curry(uncurry(f))(3))(4), 12);
    }

    #[test]
    fn test_unit_adj() {
        let u = unit_adj(99_i32);
        assert_eq!(u(42_i32), (42, 99));
    }

    #[test]
    fn test_list_bind() {
        let result = list_bind(vec![1_i32, 2], |x| vec![x * 2, x * 3]);
        assert_eq!(result, vec![2, 3, 4, 6]);
    }

    #[test]
    fn test_free_counit() {
        let r = free_counit(vec![vec![1, 2], vec![3]]);
        assert_eq!(r, vec![1, 2, 3]);
    }

    #[test]
    fn test_option_join() {
        assert_eq!(option_join(Some(Some(7_i32))), Some(7));
        assert_eq!(option_join(Some(None::<i32>)), None);
        assert_eq!(option_join(None::<Option<i32>>), None);
    }
}
(* Adjunction: F โŠฃ G means Hom(F A, B) โ‰… Hom(A, G B)
   Classic example: (- ร— C) โŠฃ (C โ†’ -)
   i.e., curry/uncurry is an adjunction *)

(* The curry/uncurry adjunction *)
let curry   f a b = f (a, b)
let uncurry f (a, b) = f a b

(* unit of the adjunction: a -> C -> (A ร— C) *)
let unit c a = (a, c)

(* counit: (A ร— C โ†’ B) ร— (A ร— C) โ†’ B *)
let counit f pair = f pair

(* Verify the adjunction: curry . uncurry = id, uncurry . curry = id *)
let () =
  let f (a, b) = a + b in
  let g a b = a * b in

  let f' = curry (uncurry f) in
  assert (f' 3 4 = f (3, 4));

  let g' = uncurry (curry g) in
  assert (g' (3, 4) = g 3 4);

  Printf.printf "curry . uncurry = id: %b\n" (f' 3 4 = f (3, 4));
  Printf.printf "uncurry . curry = id: %b\n" (g' (3, 4) = g 3 4);

  (* The product-exponential adjunction *)
  (* Hom(A ร— B, C) โ‰… Hom(A, B โ†’ C) *)
  let add_curried = curry (fun (a, b) -> a + b) in
  Printf.printf "curried add 3 4 = %d\n" (add_curried 3 4);
  let add_uncurried = uncurry (fun a b -> a + b) in
  Printf.printf "uncurried add (3,4) = %d\n" (add_uncurried (3, 4))