ExamplesBy LevelBy TopicLearning Paths
205 Advanced

Lens Modify

Functional Programming

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

  • • Implement and use over (modify) as the primary lens update operation
  • • See how over composes with other transformations: modify_each, modify_if
  • • Understand set as a special case of over: set(lens, a, s) = over(lens, |_| a, s)
  • • Practice chaining multiple over calls to apply independent updates to a structure
  • Code 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

  • Operator syntax: Haskell's %~ makes lens modification declarative; OCaml and Rust use function calls — less concise but clearer for learners.
  • Multiple modifications: Applying multiple over calls is equivalent to composing functions and applying once; lazy lenses can fuse these.
  • Shared structure: Each over call clones unchanged fields via ..record destructuring; shared persistent data structures avoid this copying.
  • Batch updates: 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Lens type('s, 'a) lensLens<S, A>
    modify signature('s,'a) lens -> ('a -> 'a) -> 's -> 'sfn modify(&self, f: impl FnOnce(A)->A, s: &S) -> S
    Transformation fn'a -> 'a (auto-curried)impl FnOnce(A) -> A (explicit trait)
    Partial applicationlet 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.
  • Partial application gap: OCaml's currying makes 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.
  • Ownership and immutability: OCaml's record update syntax ({ 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.
  • **Composition pays off at 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

  • Implement set_via_over(lens, a, s) -> S using only over (no direct access to lens.set).
  • Write modify_if(lens, pred, f, s) that applies f only if the focused value satisfies pred.
  • Chain three over calls on the same config, each modifying a different field, and verify the result is correct.
  • Open Source Repos