Lens Modify
Tutorial
The Problem
over (also called modify) is the key derived operation of a lens: apply a function to the focused field without extracting and re-inserting manually. over(lens, f, s) = lens.set(f(lens.get(s)), s). This operation is so central to lens usage that it deserves its own example. Composing over with other lens operations (modify a sub-field, accumulate changes, apply validation) shows the lens as a reusable update combinator.
🎯 Learning Outcomes
over (modify) as the primary lens update operationover composes with other transformations: modify_each, modify_ifset as a special case of over: set(lens, a, s) = over(lens, |_| a, s)over calls to apply independent updates to a structureCode Example
impl<S: 'static, A: 'static> Lens<S, A> {
pub fn modify(&self, f: impl FnOnce(A) -> A, s: &S) -> S {
(self.set)(f((self.get)(s)), s)
}
}
let lens = counter_count_lens();
let incremented = lens.modify(|n| n + 1, &counter);
let doubled = lens.modify(|n| n * 2, &counter);
let reset = lens.modify(|_| 0, &counter);Key Differences
%~ makes lens modification declarative; OCaml and Rust use function calls ā less concise but clearer for learners.over calls is equivalent to composing functions and applying once; lazy lenses can fuse these.over call clones unchanged fields via ..record destructuring; shared persistent data structures avoid this copying.modify_all(lens, f, list) maps over(lens, f) over a list ā a common pattern for bulk updates.OCaml Approach
OCaml's over (often called modify or update) is identical:
let over l f s = l.set (f (l.get s)) s
(* Usage: *)
let new_config = over port_lens (fun p -> p + 1) config
Haskell's lens library uses %~ as the infix operator for over: config & port_lens %~ (+1). OCaml's (|>) provides a similar pipeline: config |> over port_lens ((+) 1).
Full Source
#![allow(clippy::all)]
use std::rc::Rc;
type GetFn<S, A> = Box<dyn Fn(&S) -> A>;
type SetFn<S, A> = Box<dyn Fn(A, &S) -> S>;
/// A Lens<S, A> focuses on a field of type A inside a structure S.
/// `get` extracts the field; `set` returns a new S with the field replaced.
pub struct Lens<S, A> {
pub get: GetFn<S, A>,
pub set: SetFn<S, A>,
}
impl<S: 'static, A: 'static> Lens<S, A> {
pub fn new(get: impl Fn(&S) -> A + 'static, set: impl Fn(A, &S) -> S + 'static) -> Self {
Lens {
get: Box::new(get),
set: Box::new(set),
}
}
/// The key operation: look at the focused value, run it through `f`, put it back.
///
/// `modify lens f s = set lens (f (get lens s)) s`
///
/// This is more composable than `set` because you don't need the old value
/// at the call site ā the Lens fetches it for you.
pub fn modify(&self, f: impl FnOnce(A) -> A, s: &S) -> S {
(self.set)(f((self.get)(s)), s)
}
/// Compose two lenses: `self` focuses SāA, `inner` focuses AāB.
/// Result is a single Lens<S, B> that traverses both levels at once.
pub fn compose<B: 'static>(self, inner: Lens<A, B>) -> Lens<S, B>
where
A: Clone,
{
let outer_get = Rc::new(self.get);
let outer_get2 = Rc::clone(&outer_get);
let outer_set = self.set;
let inner_get = inner.get;
let inner_set = inner.set;
Lens {
get: Box::new(move |s| inner_get(&outer_get(s))),
set: Box::new(move |b, s| {
let a: A = outer_get2(s);
let a2 = inner_set(b, &a);
outer_set(a2, s)
}),
}
}
}
// ---------------------------------------------------------------------------
// Domain types
// ---------------------------------------------------------------------------
#[derive(Clone, Debug, PartialEq)]
pub struct Counter {
pub count: i64,
pub label: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Item {
pub name: String,
pub price: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Cart {
pub item: Item,
pub quantity: u32,
}
// ---------------------------------------------------------------------------
// Lenses
// ---------------------------------------------------------------------------
pub fn counter_count_lens() -> Lens<Counter, i64> {
Lens::new(
|c: &Counter| c.count,
|n, c| Counter {
count: n,
..c.clone()
},
)
}
pub fn counter_label_lens() -> Lens<Counter, String> {
Lens::new(
|c: &Counter| c.label.clone(),
|l, c| Counter {
label: l,
..c.clone()
},
)
}
pub fn cart_item_lens() -> Lens<Cart, Item> {
Lens::new(
|cart: &Cart| cart.item.clone(),
|item, cart| Cart {
item,
..cart.clone()
},
)
}
pub fn item_price_lens() -> Lens<Item, f64> {
Lens::new(
|i: &Item| i.price,
|p, i| Item {
price: p,
..i.clone()
},
)
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
fn sample_counter() -> Counter {
Counter {
count: 5,
label: "clicks".into(),
}
}
fn sample_cart() -> Cart {
Cart {
item: Item {
name: "widget".into(),
price: 10.0,
},
quantity: 3,
}
}
#[test]
fn test_modify_increment() {
let lens = counter_count_lens();
let c = sample_counter();
let updated = lens.modify(|n| n + 1, &c);
assert_eq!(updated.count, 6);
// Other fields unchanged
assert_eq!(updated.label, "clicks");
}
#[test]
fn test_modify_double() {
let lens = counter_count_lens();
let c = sample_counter();
let updated = lens.modify(|n| n * 2, &c);
assert_eq!(updated.count, 10);
}
#[test]
fn test_modify_reset_to_zero() {
let lens = counter_count_lens();
let c = sample_counter();
let updated = lens.modify(|_| 0, &c);
assert_eq!(updated.count, 0);
assert_eq!(updated.label, "clicks");
}
#[test]
fn test_modify_string_field() {
let lens = counter_label_lens();
let c = sample_counter();
let updated = lens.modify(|s| s.to_uppercase(), &c);
assert_eq!(updated.label, "CLICKS");
assert_eq!(updated.count, 5);
}
#[test]
fn test_modify_negative_count() {
let lens = counter_count_lens();
let c = Counter {
count: 3,
label: "x".into(),
};
let updated = lens.modify(|n| -n, &c);
assert_eq!(updated.count, -3);
}
#[test]
fn test_modify_chained() {
// Apply modify twice in sequence ā each step is independent
let lens = counter_count_lens();
let c = sample_counter();
let step1 = lens.modify(|n| n + 1, &c); // 5 ā 6
let step2 = lens.modify(|n| n * 2, &step1); // 6 ā 12
assert_eq!(step2.count, 12);
// original untouched
assert_eq!(c.count, 5);
}
#[test]
fn test_modify_through_composed_lens() {
// Lens<Cart, Item> composed with Lens<Item, f64> ā Lens<Cart, f64>
// modify doubles the price inside a cart
let cart_price = cart_item_lens().compose(item_price_lens());
let cart = sample_cart(); // price = 10.0
let updated = cart_price.modify(|p| p * 2.0, &cart);
assert_eq!(updated.item.price, 20.0);
// Unchanged fields
assert_eq!(updated.item.name, "widget");
assert_eq!(updated.quantity, 3);
}
#[test]
fn test_modify_does_not_mutate_original() {
let lens = counter_count_lens();
let original = sample_counter();
let _updated = lens.modify(|n| n + 100, &original);
// original is still 5
assert_eq!(original.count, 5);
}
#[test]
fn test_modify_identity_function() {
// modify with identity function returns an equivalent struct
let lens = counter_count_lens();
let c = sample_counter();
let updated = lens.modify(|n| n, &c);
assert_eq!(updated, c);
}
}#[cfg(test)]
mod tests {
use super::*;
fn sample_counter() -> Counter {
Counter {
count: 5,
label: "clicks".into(),
}
}
fn sample_cart() -> Cart {
Cart {
item: Item {
name: "widget".into(),
price: 10.0,
},
quantity: 3,
}
}
#[test]
fn test_modify_increment() {
let lens = counter_count_lens();
let c = sample_counter();
let updated = lens.modify(|n| n + 1, &c);
assert_eq!(updated.count, 6);
// Other fields unchanged
assert_eq!(updated.label, "clicks");
}
#[test]
fn test_modify_double() {
let lens = counter_count_lens();
let c = sample_counter();
let updated = lens.modify(|n| n * 2, &c);
assert_eq!(updated.count, 10);
}
#[test]
fn test_modify_reset_to_zero() {
let lens = counter_count_lens();
let c = sample_counter();
let updated = lens.modify(|_| 0, &c);
assert_eq!(updated.count, 0);
assert_eq!(updated.label, "clicks");
}
#[test]
fn test_modify_string_field() {
let lens = counter_label_lens();
let c = sample_counter();
let updated = lens.modify(|s| s.to_uppercase(), &c);
assert_eq!(updated.label, "CLICKS");
assert_eq!(updated.count, 5);
}
#[test]
fn test_modify_negative_count() {
let lens = counter_count_lens();
let c = Counter {
count: 3,
label: "x".into(),
};
let updated = lens.modify(|n| -n, &c);
assert_eq!(updated.count, -3);
}
#[test]
fn test_modify_chained() {
// Apply modify twice in sequence ā each step is independent
let lens = counter_count_lens();
let c = sample_counter();
let step1 = lens.modify(|n| n + 1, &c); // 5 ā 6
let step2 = lens.modify(|n| n * 2, &step1); // 6 ā 12
assert_eq!(step2.count, 12);
// original untouched
assert_eq!(c.count, 5);
}
#[test]
fn test_modify_through_composed_lens() {
// Lens<Cart, Item> composed with Lens<Item, f64> ā Lens<Cart, f64>
// modify doubles the price inside a cart
let cart_price = cart_item_lens().compose(item_price_lens());
let cart = sample_cart(); // price = 10.0
let updated = cart_price.modify(|p| p * 2.0, &cart);
assert_eq!(updated.item.price, 20.0);
// Unchanged fields
assert_eq!(updated.item.name, "widget");
assert_eq!(updated.quantity, 3);
}
#[test]
fn test_modify_does_not_mutate_original() {
let lens = counter_count_lens();
let original = sample_counter();
let _updated = lens.modify(|n| n + 100, &original);
// original is still 5
assert_eq!(original.count, 5);
}
#[test]
fn test_modify_identity_function() {
// modify with identity function returns an equivalent struct
let lens = counter_count_lens();
let c = sample_counter();
let updated = lens.modify(|n| n, &c);
assert_eq!(updated, c);
}
}
Deep Comparison
OCaml vs Rust: Lens Modify ā Transform a Field With a Function
Side-by-Side Code
OCaml
type ('s, 'a) lens = {
get : 's -> 'a;
set : 'a -> 's -> 's;
}
let modify (l : ('s, 'a) lens) (f : 'a -> 'a) (s : 's) : 's =
l.set (f (l.get s)) s
type counter = { count : int; label : string }
let count_lens = {
get = (fun c -> c.count);
set = (fun n c -> { c with count = n });
}
let increment = modify count_lens (( + ) 1)
let double = modify count_lens (( * ) 2)
let reset = modify count_lens (fun _ -> 0)
Rust (idiomatic ā method on struct)
impl<S: 'static, A: 'static> Lens<S, A> {
pub fn modify(&self, f: impl FnOnce(A) -> A, s: &S) -> S {
(self.set)(f((self.get)(s)), s)
}
}
let lens = counter_count_lens();
let incremented = lens.modify(|n| n + 1, &counter);
let doubled = lens.modify(|n| n * 2, &counter);
let reset = lens.modify(|_| 0, &counter);
Rust (composed ā modify through two levels)
// cart_item_lens: Lens<Cart, Item>
// item_price_lens: Lens<Item, f64>
// composed: Lens<Cart, f64>
let cart_price = cart_item_lens().compose(item_price_lens());
let discounted = cart_price.modify(|p| p * 0.9, &cart);
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Lens type | ('s, 'a) lens | Lens<S, A> |
modify signature | ('s,'a) lens -> ('a -> 'a) -> 's -> 's | fn modify(&self, f: impl FnOnce(A)->A, s: &S) -> S |
| Transformation fn | 'a -> 'a (auto-curried) | impl FnOnce(A) -> A (explicit trait) |
| Partial application | let increment = modify count_lens ((+) 1) | requires a closure or helper fn |
| Struct update | { c with count = n } | Counter { count: n, ..c.clone() } |
Key Insights
modify = set ā f ā get**: Both languages implement the same three-step pipeline ā fetch, transform, replace. The implementations are structurally identical; only the syntax differs.let increment = modify count_lens ((+) 1) natural ā it returns a function counter -> counter. Rust requires an explicit closure or a helper struct to achieve the same partially-applied combinator, because Rust functions are not auto-curried.{ c with count = n }) performs an implicit shallow copy. Rust spells this out with ..c.clone(), making the allocation visible. modify takes &S (borrow) and returns a new owned S, matching OCaml's persistent/immutable data style.FnOnce vs Fn**: The transformation f is consumed once per call (FnOnce), which is the most general Rust closure bound. If you need to call modify repeatedly with the same closure stored somewhere, you'd use Fn instead ā a trade-off OCaml never surfaces because closures are always copyable values there.modify time**: A composed Lens<Cart, f64> lets you call modify on the nested price field with exactly the same API as a flat lens. No extra boilerplate, no repeated navigation code ā the composition glues the path together once and modify works uniformly at any depth.When to Use Each Style
**Use modify when:** you need to transform a field rather than replace it with a literal ā incrementing counters, scaling prices, uppercasing strings, appending to lists. Any time the new value depends on the old one, modify is cleaner than set(lens, f(get(lens, s)), s).
**Use composed modify when:** the field to transform is nested two or more levels deep. Compose the path lenses once, then call modify on the result ā the navigation logic lives in one place and the transformation logic lives at the call site.
Exercises
set_via_over(lens, a, s) -> S using only over (no direct access to lens.set).modify_if(lens, pred, f, s) that applies f only if the focused value satisfies pred.over calls on the same config, each modifying a different field, and verify the result is correct.