Example 1081: Lenses
Difficulty: โญโญโญ Category: Higher-order functions OCaml Source: Real World OCamlProblem 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
- How OCaml's record-of-closures pattern translates to Rust structs with boxed closures
- Composing lenses via `Arc`-shared closures to focus through multiple levels of nesting
- The three lens laws (get-set, set-get, set-set) as property-based test foundations
- Why Rust needs explicit `Clone` bounds where OCaml's GC handles structural sharing implicitly
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_lensRust (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
| Concept | OCaml | Rust |
|---|---|---|
| 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.