๐Ÿฆ€ Functional Rust

Example 1081: Lenses

Difficulty: โญโญโญ Category: Higher-order functions OCaml Source: Real World OCaml

Problem Statement

Implement lenses โ€” composable functional getters and setters for nested record updates. A lens focuses on a specific field of a structure, allowing you to get, set, and transform it without mutation.

Learning Outcomes

OCaml Approach

OCaml defines a lens as a record with `get` and `set` closures. Composition is trivial: thread the inner lens through the outer lens's get/set. The `{ p with addr = a }` syntax creates a new record cheaply. Garbage collection handles all intermediate values.

Rust Approach

Rust uses a struct with `Box<dyn Fn>` closures for the same pattern. Composition requires `Arc` to share the inner lens and outer closures between the composed getter and setter. The setter path needs `Clone` to build new values since Rust has no `{ ..p }` for arbitrary cloning. All updates are pure โ€” the original value is never mutated.

Key Differences

1. Record update syntax: OCaml `{ p with field = v }` is built-in; Rust requires manually constructing a new struct and cloning unchanged fields. 2. Closure sharing: OCaml closures are GC-managed values; Rust needs `Arc` to share closures between composed getter and setter. 3. Type bounds: OCaml's parametric polymorphism just works; Rust requires `'static + Clone` bounds to store closures in boxes and rebuild values. 4. Lifetime threading: OCaml's GC makes composed getters trivial; Rust's composed getter must carefully thread lifetimes through two layers of boxed closures.
/// A lens is a pair of getter/setter functions that focus on a part of a larger structure.
/// This is the functional approach to accessing and modifying nested data without mutation.

// Type aliases for lens closure types
type Getter<S, A> = Box<dyn Fn(&S) -> &A>;
type Setter<S, A> = Box<dyn Fn(A, &S) -> S>;

struct Lens<S, A> {
    get_fn: Getter<S, A>,
    set_fn: Setter<S, A>,
}

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

    fn get<'s>(&self, whole: &'s S) -> &'s A {
        (self.get_fn)(whole)
    }

    fn set(&self, value: A, whole: &S) -> S {
        (self.set_fn)(value, whole)
    }

    fn compose<B: 'static>(self, inner: Lens<A, B>) -> Lens<S, B>
    where
        S: Clone,
        A: Clone,
    {
        let inner = std::sync::Arc::new(inner);
        let outer_get = std::sync::Arc::new(self.get_fn);
        let outer_set = std::sync::Arc::new(self.set_fn);

        let get_outer = std::sync::Arc::clone(&outer_get);
        let get_inner = std::sync::Arc::clone(&inner);

        let set_outer_get = std::sync::Arc::clone(&outer_get);
        let set_outer_set = std::sync::Arc::clone(&outer_set);
        let set_inner = std::sync::Arc::clone(&inner);

        Lens::new(
            move |s: &S| {
                let mid: &A = (get_outer)(s);
                let mid_ptr: *const A = mid;
                (get_inner.get_fn)(unsafe { &*mid_ptr })
            },
            move |b: B, s: &S| {
                let mid: &A = (set_outer_get)(s);
                let new_mid: A = (set_inner.set_fn)(b, mid);
                (set_outer_set)(new_mid, s)
            },
        )
    }
}

/// Apply a function to the focused part of a lens, returning an updated whole.
fn over<S: 'static, A: Clone + 'static>(
    lens: &Lens<S, A>,
    f: impl FnOnce(A) -> A,
    whole: &S,
) -> S {
    let current = lens.get(whole).clone();
    lens.set(f(current), whole)
}

// --- Domain types ---

#[derive(Debug, Clone, PartialEq)]
struct Address {
    street: String,
    city: String,
}

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

fn addr_lens() -> Lens<Person, Address> {
    Lens::new(
        |p: &Person| &p.addr,
        |a: Address, p: &Person| Person {
            name: p.name.clone(),
            addr: a,
        },
    )
}

fn city_lens() -> Lens<Address, String> {
    Lens::new(
        |a: &Address| &a.city,
        |c: String, a: &Address| Address {
            street: a.street.clone(),
            city: c,
        },
    )
}

fn person_city_lens() -> Lens<Person, String> {
    addr_lens().compose(city_lens())
}

fn main() {
    let p = Person {
        name: "Alice".to_string(),
        addr: Address {
            street: "Main St".to_string(),
            city: "NYC".to_string(),
        },
    };

    let lens = person_city_lens();

    println!("City: {}", lens.get(&p));

    let p2 = over(&lens, |c| c.to_uppercase(), &p);
    println!("City after over(uppercase): {}", lens.get(&p2));

    let p3 = lens.set("Boston".to_string(), &p);
    println!("City after set: {}", lens.get(&p3));
    println!("Original unchanged: {}", lens.get(&p));
}

/* Output:
   City: NYC
   City after over(uppercase): NYC
   City after set: Boston
   Original unchanged: NYC
*/
type ('s, 'a) lens = {
  get: 's -> 'a;
  set: 'a -> 's -> 's;
}

let compose outer inner = {
  get = (fun s -> inner.get (outer.get s));
  set = (fun a s -> outer.set (inner.set a (outer.get s)) s);
}

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

type address = { street: string; city: string }
type person = { name: string; addr: address }

let addr_lens = { get = (fun p -> p.addr); set = (fun a p -> { p with addr = a }) }
let city_lens = { get = (fun a -> a.city); set = (fun c a -> { a with city = c }) }
let person_city = compose addr_lens city_lens

let () =
  let p = { name = "Alice"; addr = { street = "Main St"; city = "NYC" } } in

  (* Test get through composed lens *)
  assert (person_city.get p = "NYC");

  (* Test set through composed lens *)
  let p2 = person_city.set "Boston" p in
  assert (p2.addr.city = "Boston");
  assert (p2.name = "Alice");
  assert (p2.addr.street = "Main St");

  (* Test over โ€” apply function to focused value *)
  let p3 = over person_city String.uppercase_ascii p in
  assert (person_city.get p3 = "NYC");

  (* Original unchanged *)
  assert (person_city.get p = "NYC");

  (* Lens law: set-get *)
  let p4 = person_city.set "Denver" p in
  assert (person_city.get p4 = "Denver");

  (* Lens law: get-set *)
  let city = person_city.get p in
  let p5 = person_city.set city p in
  assert (p5 = p);

  (* Lens law: set-set *)
  let p6 = person_city.set "B" (person_city.set "A" p) in
  let p7 = person_city.set "B" p in
  assert (p6 = p7);

  Printf.printf "City: %s\n" (person_city.get p);
  let p_upper = over person_city String.uppercase_ascii p in
  Printf.printf "City: %s\n" (person_city.get p_upper);
  print_endline "ok"

๐Ÿ“Š Detailed Comparison

OCaml vs Rust: Lenses

Side-by-Side Code

OCaml

๐Ÿช Show OCaml equivalent
type ('s, 'a) lens = {
get: 's -> 'a;
set: 'a -> 's -> 's;
}

let compose outer inner = {
get = (fun s -> inner.get (outer.get s));
set = (fun a s -> outer.set (inner.set a (outer.get s)) s);
}

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

let addr_lens = { get = (fun p -> p.addr); set = (fun a p -> { p with addr = a }) }
let city_lens = { get = (fun a -> a.city); set = (fun c a -> { a with city = c }) }
let person_city = compose addr_lens city_lens

Rust (idiomatic)

type Getter<S, A> = Box<dyn Fn(&S) -> &A>;
type Setter<S, A> = Box<dyn Fn(A, &S) -> S>;

struct Lens<S, A> {
 get_fn: Getter<S, A>,
 set_fn: Setter<S, A>,
}

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

 fn get<'s>(&self, whole: &'s S) -> &'s A { (self.get_fn)(whole) }
 fn set(&self, value: A, whole: &S) -> S { (self.set_fn)(value, whole) }
}

fn over<S: 'static, A: Clone + 'static>(lens: &Lens<S, A>, f: impl FnOnce(A) -> A, whole: &S) -> S {
 let current = lens.get(whole).clone();
 lens.set(f(current), whole)
}

Rust (composed lens via Arc)

fn compose<B: 'static>(self, inner: Lens<A, B>) -> Lens<S, B>
where S: Clone, A: Clone {
 let inner = Arc::new(inner);
 let outer_get = Arc::new(self.get_fn);
 let outer_set = Arc::new(self.set_fn);
 // ... share Arcs between get and set closures
 Lens::new(
     move |s| { /* get outer, then get inner */ },
     move |b, s| { /* get outer, set inner, set outer */ },
 )
}

Type Signatures

ConceptOCamlRust
Lens type`('s, 'a) lens``Lens<S, A>`
Get`'s -> 'a``Fn(&S) -> &A`
Set`'a -> 's -> 's``Fn(A, &S) -> S`
Compose`('s, 'a) lens -> ('a, 'b) lens -> ('s, 'b) lens``Lens<S, A> -> Lens<A, B> -> Lens<S, B>`
Over`('s, 'a) lens -> ('a -> 'a) -> 's -> 's``&Lens<S, A>, FnOnce(A) -> A, &S -> S`
Record update`{ p with field = v }`Manual struct construction + Clone

Key Insights

1. Closures as values: OCaml's record-of-closures maps directly to Rust's struct-of-boxed-closures, but Rust requires explicit `'static` bounds for boxed trait objects. This is the fundamental cost of no GC โ€” you must prove closure lifetimes to the compiler.

2. Composition needs sharing: OCaml's `compose` creates two new closures that capture the inner lens. In Rust, the inner lens must be wrapped in `Arc` because both the composed getter and setter need access to it, and closures can't share ownership without reference counting.

3. Borrowing vs copying in get: OCaml's `get` returns a value (copied by the GC). Rust's `get` returns a `&A` reference into the original structure, avoiding allocation. This is more efficient but makes composition harder โ€” the composed getter must thread lifetimes through two levels of indirection.

4. Set requires Clone: OCaml's `{ p with addr = a }` is syntactic sugar that cheaply creates a new record. Rust has no equivalent โ€” you must manually construct a new struct and `Clone` every unchanged field. This makes the `Clone` bound mandatory on the set path.

5. Lens laws hold in both: The three lens laws (get-set, set-get, set-set) are preserved identically. Functional purity means the same equational reasoning applies regardless of language. The tests verify all three laws.

When to Use Each Style

Use idiomatic Rust lenses when: you have deeply nested structures that need frequent functional updates, especially in state management for UI frameworks or game engines where immutability prevents bugs.

Use OCaml-style lenses when: you're writing OCaml or want to understand the theoretical foundation. OCaml's GC and structural equality make lenses almost zero-cost to define and compose, which is why they're more common in ML-family languages.