๐Ÿฆ€ Functional Rust

239: Strong Profunctor

Difficulty: โญโญโญโญ Level: Category Theory A strong profunctor is a function-like thing that can "carry extra context" โ€” the categorical foundation of lenses and the entire optics hierarchy.

The Problem This Solves

You have a function `fn(Name) -> Name` (transform a name). You want to use it to transform the `name` field inside a `User` struct, leaving all other fields unchanged. Normally you'd write boilerplate: get the field, apply the function, set it back. With lenses, this is automatic and composable. But what makes a lens work at the type level? The answer is Strong profunctors. A profunctor is like a function that can be mapped on both input (contravariantly) and output (covariantly). A strong profunctor additionally knows how to "pass along" extra context โ€” the other fields it doesn't touch โ€” through the transformation. This is exactly the capability lenses need: focus on part `A` inside whole `S`, transform `A`, and reconstruct `S` with everything else unchanged. Understanding strong profunctors explains why the lens laws are the laws they are and why lenses compose โ€” composition of profunctor morphisms.

The Intuition

A profunctor `P<A, B>` is like a transformer from `A` to `B`, but it can be adapted at both ends: The canonical profunctor is just `fn(A) -> B`. A strong profunctor additionally has: Think of `first` as: "I'm a camera lens focused on the first element of a pair. I transform it; the second element passes through untouched." A lens is then: `Lens<S, A>` = for any strong profunctor `P`, lift `P<A, A>` to `P<S, S>`. Concretely, it needs a `get: S -> A` (extract the field) and a `set: (S, A) -> S` (put back a new value). The `strong` capability provides the "carry the rest of S through" machinery.

How It Works in Rust

/// The canonical strong profunctor: just a function
pub struct Mapper<A, B> {
 f: Box<dyn Fn(A) -> B>,
}

impl<A: 'static, B: 'static> Mapper<A, B> {
 /// first: lift P(A,B) to P((A,C),(B,C)) โ€” C passes through untouched
 pub fn first<C: 'static>(self) -> Mapper<(A, C), (B, C)> {
     let f = self.f;
     Mapper::new(move |(a, c)| (f(a), c))  // transform a, carry c
 }

 /// second: lift P(A,B) to P((C,A),(C,B)) โ€” C passes through on the left
 pub fn second<C: 'static>(self) -> Mapper<(C, A), (C, B)> {
     let f = self.f;
     Mapper::new(move |(c, a)| (c, f(a)))  // carry c, transform a
 }

 /// dimap: adapt both input and output
 pub fn dimap<C: 'static, D: 'static>(
     self,
     pre:  impl Fn(C) -> A + 'static,  // preprocess input
     post: impl Fn(B) -> D + 'static,  // postprocess output
 ) -> Mapper<C, D> {
     let f = self.f;
     Mapper::new(move |c| post(f(pre(c))))
 }
}

/// A lens: focus on field A inside whole S
pub struct Lens<S, A> {
 pub get: Box<dyn Fn(&S) -> A>,
 pub set: Box<dyn Fn(S, A) -> S>,
}

impl<S: Clone + 'static, A: Clone + 'static> Lens<S, A> {
 /// Apply a Mapper<A,A> through the lens to get Mapper<S,S>
 pub fn apply(&self, mapper: Mapper<A, A>) -> Mapper<S, S> {
     let get = self.get.clone_box();  // S -> A
     let set = self.set.clone_box();  // (S, A) -> S
     // Use first/dimap to: extract A, transform it, put it back
     mapper
         .dimap(
             move |s: S| (get(&s), s),     // S -> (A, S)  -- extract + carry whole
             move |(new_a, s)| set(s, new_a), // (A, S) -> S  -- reconstruct
         )
 }
}

What This Unlocks

Key Differences

ConceptOCamlRust
ProfunctorModule type `PROFUNCTOR``trait Profunctor` / concrete `Mapper<A,B>`
Strong extension`first` / `second` in module sigMethods on `Mapper`
Lens encodingVan Laarhoven `(a -> f a) -> s -> f s``struct Lens { get, set }` + `apply`
DimapHigher-kinded functionMethod on `Mapper` with owned `self`
CompositionModule functorMethod chaining / `compose` function
/// Strong Profunctor โ€” enables lenses.
///
/// A Strong profunctor P<A,B> can "pass along" extra context:
///
///   first  :: P A B -> P (A, C) (B, C)   -- focus on the first element
///   second :: P A B -> P (C, A) (C, B)   -- focus on the second element
///
/// The canonical strong profunctor is functions `fn(A) -> B`.
///
/// Strong profunctors enable the optics hierarchy:
///   Lens s a = โˆ€ p. Strong p => p a a -> p s s
///
/// A lens focuses on a part `a` of a whole `s`.
/// It requires `Strong` because we need to "carry the rest of s" through.

use std::rc::Rc;

/// A function-wrapper as the canonical Strong Profunctor.
pub struct Mapper<A, B> {
    f: Box<dyn Fn(A) -> B>,
}

impl<A: 'static, B: 'static> Mapper<A, B> {
    pub fn new(f: impl Fn(A) -> B + 'static) -> Self {
        Mapper { f: Box::new(f) }
    }

    pub fn apply(&self, a: A) -> B {
        (self.f)(a)
    }

    /// first: lift P(A,B) to P((A,C),(B,C)) โ€” keep the C component unchanged.
    pub fn first<C: 'static>(self) -> Mapper<(A, C), (B, C)> {
        let f = self.f;
        Mapper::new(move |(a, c)| (f(a), c))
    }

    /// second: lift P(A,B) to P((C,A),(C,B))
    pub fn second<C: 'static>(self) -> Mapper<(C, A), (C, B)> {
        let f = self.f;
        Mapper::new(move |(c, a)| (c, f(a)))
    }

    /// dimap for composition
    pub fn dimap<C: 'static, D: 'static>(
        self,
        pre: impl Fn(C) -> A + 'static,
        post: impl Fn(B) -> D + 'static,
    ) -> Mapper<C, D> {
        let f = self.f;
        Mapper::new(move |c| post(f(pre(c))))
    }
}

// โ”€โ”€ Lens โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// A concrete lens: focuses on part `A` inside whole `S`.
pub struct Lens<S, A> {
    pub get: Box<dyn Fn(&S) -> A>,
    pub set: Box<dyn Fn(S, A) -> S>,
}

impl<S: Clone + 'static, A: Clone + 'static> Lens<S, A> {
    pub fn new(
        get: impl Fn(&S) -> A + 'static,
        set: impl Fn(S, A) -> S + 'static,
    ) -> Self {
        Lens {
            get: Box::new(get),
            set: Box::new(set),
        }
    }

    /// view: get the focused value
    pub fn view(&self, s: &S) -> A {
        (self.get)(s)
    }

    /// set: replace the focused value
    pub fn set_val(&self, s: S, a: A) -> S {
        (self.set)(s, a)
    }

    /// over: modify the focused value
    pub fn over(&self, s: S, f: impl Fn(A) -> A) -> S {
        let a = (self.get)(&s);
        (self.set)(s, f(a))
    }

    /// Compose two lenses: focus on S -> T -> A
    pub fn compose<T: Clone + 'static>(
        outer: Lens<S, T>,
        inner: Lens<T, A>,
    ) -> Lens<S, A> {
        let outer1 = Rc::new(outer);
        let inner1 = Rc::new(inner);
        let outer2 = outer1.clone();
        let inner2 = inner1.clone();
        Lens::new(
            move |s| inner1.view(&outer1.view(s)),
            move |s, a| {
                let t = outer2.view(&s);
                let new_t = inner2.set_val(t, a);
                outer2.set_val(s, new_t)
            },
        )
    }
}

// โ”€โ”€ Example data structures โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

#[derive(Debug, Clone, PartialEq)]
struct Person {
    name: String,
    age: u32,
}

#[derive(Debug, Clone, PartialEq)]
struct Company {
    ceo: Person,
    revenue: i64,
}

fn main() {
    println!("=== Strong Profunctor + Lenses ===\n");
    println!("Strong: first :: P A B -> P (A,C) (B,C)");
    println!("Enables lenses: focus on parts of a structure.\n");

    // Strong profunctor: first and second
    let double = Mapper::new(|n: i32| n * 2);
    let double_first = double.first::<String>();
    let result = double_first.apply((21, "hello".to_string()));
    println!("first (double) applied to (21, \"hello\"): {:?}", result);

    let add_one = Mapper::new(|n: i32| n + 1);
    let add_one_second = add_one.second::<&str>();
    let result2 = add_one_second.apply(("world", 41));
    println!("second (add_one) applied to (\"world\", 41): {:?}", result2);

    // Lenses
    println!();
    let name_lens: Lens<Person, String> = Lens::new(
        |p: &Person| p.name.clone(),
        |mut p: Person, n| { p.name = n; p },
    );

    let age_lens: Lens<Person, u32> = Lens::new(
        |p: &Person| p.age,
        |mut p: Person, a| { p.age = a; p },
    );

    let ceo_lens: Lens<Company, Person> = Lens::new(
        |c: &Company| c.ceo.clone(),
        |mut c: Company, p| { c.ceo = p; c },
    );

    let p = Person { name: "Alice".to_string(), age: 45 };
    let c = Company { ceo: p.clone(), revenue: 1_000_000 };

    // view
    println!("CEO name: {}", name_lens.view(&c.ceo));
    println!("CEO age: {}", age_lens.view(&c.ceo));

    // set via lens
    let c2 = ceo_lens.set_val(c.clone(), {
        name_lens.set_val(c.ceo.clone(), "Bob".to_string())
    });
    println!("After set CEO name: {}", name_lens.view(&c2.ceo));

    // over (modify)
    let c3 = ceo_lens.over(c.clone(), |p| age_lens.over(p, |a| a + 1));
    println!("After birthday: age = {}", age_lens.view(&c3.ceo));

    // Composed lens: Company -> Person -> String
    let ceo_name_lens = Lens::compose(
        Lens::new(|c: &Company| c.ceo.clone(), |mut c, p| { c.ceo = p; c }),
        Lens::new(|p: &Person| p.name.clone(), |mut p, n| { p.name = n; p }),
    );
    println!("\nComposed lens (company -> ceo -> name): {}", ceo_name_lens.view(&c));
    let c4 = ceo_name_lens.set_val(c, "Carol".to_string());
    println!("After set via composed lens: {}", ceo_name_lens.view(&c4));

    println!();
    println!("The `first` operation is the key: it lets a profunctor carry context.");
    println!("A lens IS a profunctor transformation: Lens s a = โˆ€p. Strong p => p a a -> p s s");
}

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

    #[test]
    fn test_first() {
        let double = Mapper::new(|n: i32| n * 2);
        let r = double.first::<&str>().apply((5, "x"));
        assert_eq!(r, (10, "x"));
    }

    #[test]
    fn test_second() {
        let inc = Mapper::new(|n: i32| n + 10);
        let r = inc.second::<&str>().apply(("tag", 5));
        assert_eq!(r, ("tag", 15));
    }

    #[test]
    fn test_lens_view_set() {
        let p = Person { name: "Alice".to_string(), age: 30 };
        let age_lens: Lens<Person, u32> = Lens::new(
            |p| p.age,
            |mut p, a| { p.age = a; p },
        );
        assert_eq!(age_lens.view(&p), 30);
        let p2 = age_lens.set_val(p, 31);
        assert_eq!(age_lens.view(&p2), 31);
    }

    #[test]
    fn test_lens_over() {
        let name_lens: Lens<Person, String> = Lens::new(
            |p| p.name.clone(),
            |mut p, n| { p.name = n; p },
        );
        let p = Person { name: "alice".to_string(), age: 0 };
        let p2 = name_lens.over(p, |n| n.to_uppercase());
        assert_eq!(p2.name, "ALICE");
    }

    #[test]
    fn test_composed_lens() {
        let ceo_lens = Lens::new(
            |c: &Company| c.ceo.clone(),
            |mut c, p| { c.ceo = p; c },
        );
        let name_lens = Lens::new(
            |p: &Person| p.name.clone(),
            |mut p, n| { p.name = n; p },
        );
        let ceo_name = Lens::compose(ceo_lens, name_lens);
        let c = Company {
            ceo: Person { name: "Old".to_string(), age: 50 },
            revenue: 0,
        };
        let c2 = ceo_name.set_val(c, "New".to_string());
        assert_eq!(c2.ceo.name, "New");
    }
}
(* A strong profunctor can carry extra information through computation.
   first  :: p a b -> p (a * c) (b * c)
   second :: p a b -> p (c * a) (c * b)
   Enables building lenses! *)

(* Functions form a strong profunctor *)
let first  f (a, c) = (f a, c)
let second f (c, a) = (c, f a)

(* Lens from strong profunctor *)
(* A lens s a = forall p. Strong p => p a a -> p s s *)
(* Simplified: a pair of get/set *)
type ('s, 'a) lens = {
  get : 's -> 'a;
  set : 'a -> 's -> 's;
}

let view lens s = lens.get s
let over lens f s = lens.set (f (lens.get s)) s
let set  lens a s = lens.set a s

(* Compose lenses *)
let compose l1 l2 = {
  get = (fun s -> l2.get (l1.get s));
  set = (fun a s -> l1.set (l2.set a (l1.get s)) s);
}

type person = { name: string; age: int }
type company = { ceo: person; revenue: int }

let name_lens = { get = (fun p -> p.name); set = (fun n p -> { p with name = n }) }
let ceo_lens  = { get = (fun c -> c.ceo);  set = (fun p c -> { c with ceo = p }) }
let ceo_name  = compose ceo_lens name_lens

let () =
  let c = { ceo = { name = "Alice"; age = 45 }; revenue = 1_000_000 } in
  Printf.printf "CEO: %s\n" (view ceo_name c);
  let c' = set ceo_name "Bob" c in
  Printf.printf "New CEO: %s\n" (view ceo_name c');
  let c'' = over ceo_lens (fun p -> { p with age = p.age + 1 }) c in
  Printf.printf "CEO age after bday: %d\n" c''.ceo.age