ExamplesBy LevelBy TopicLearning Paths
1086 Advanced

Lenses — Functional Getters and Setters

Functional Abstractions

Tutorial Video

Text description (accessibility)

This video demonstrates the "Lenses — Functional Getters and Setters" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Abstractions. Implement lenses — composable, first-class getter/setter pairs that allow reading and updating deeply nested immutable data structures without boilerplate. Key difference from OCaml: 1. **Closure storage:** OCaml records hold functions directly; Rust requires `Box<dyn Fn>` for type erasure and heap allocation.

Tutorial

The Problem

Implement lenses — composable, first-class getter/setter pairs that allow reading and updating deeply nested immutable data structures without boilerplate.

🎯 Learning Outcomes

  • • How to encode OCaml record-of-functions as a Rust struct holding boxed closures
  • • Lens composition via compose — chaining focus through nested structures
  • • The over combinator as a functional "modify in place" on immutable data
  • • Lens laws (get-set, set-get, set-set) and how they guarantee correctness
  • 🦀 The Rust Way

    Rust models a lens as a struct with two Box<dyn Fn(...)> fields. Composition consumes both lenses and uses Rc to share the closure pointers between the composed getter and setter. The over combinator applies a transformation function through the lens. Immutable updates are done by constructing new structs — Rust has no { p with ... } syntax, so fields are rebuilt explicitly.

    Code Example

    pub struct Lens<S, A> {
        getter: Box<dyn Fn(&S) -> A>,
        setter: Box<dyn Fn(A, &S) -> S>,
    }
    
    impl<S: 'static, A: 'static> Lens<S, A> {
        pub fn new(
            getter: impl Fn(&S) -> A + 'static,
            setter: impl Fn(A, &S) -> S + 'static,
        ) -> Self {
            Lens { getter: Box::new(getter), setter: Box::new(setter) }
        }
    
        pub fn get(&self, s: &S) -> A { (self.getter)(s) }
        pub fn set(&self, a: A, s: &S) -> S { (self.setter)(a, s) }
        pub fn over(&self, f: impl Fn(A) -> A, s: &S) -> S {
            self.set(f(self.get(s)), s)
        }
    
        pub fn compose<B: 'static>(self, inner: Lens<A, B>) -> Lens<S, B>
        where A: Clone {
            use std::rc::Rc;
            let og = Rc::new(self.getter);
            let os = Rc::new(self.setter);
            let ig = Rc::new(inner.getter);
            let is_ = Rc::new(inner.setter);
    
            let (og1, ig1) = (Rc::clone(&og), Rc::clone(&ig));
            let (og2, os2, is2) = (Rc::clone(&og), Rc::clone(&os), Rc::clone(&is_));
    
            Lens::new(
                move |s: &S| ig1(&og1(s)),
                move |b, s: &S| { let a = og2(s); os2(is2(b, &a), s) },
            )
        }
    }

    Key Differences

  • Closure storage: OCaml records hold functions directly; Rust requires Box<dyn Fn> for type erasure and heap allocation.
  • Composition ownership: OCaml freely copies closures; Rust compose consumes both lenses and wraps internals in Rc so getter and setter can share access.
  • Immutable update syntax: OCaml has { record with field = value }; Rust reconstructs the entire struct.
  • Clone requirements: OCaml copies values implicitly; Rust needs explicit .clone() when the getter must return an owned value from a borrow.
  • OCaml Approach

    OCaml models a lens as a record with get and set fields — both are plain functions. Composition is a function that takes two lens records and returns a new one whose get chains inner after outer, and whose set threads the update back through both layers. Record-with update ({ p with addr = a }) makes immutable updates concise.

    Full Source

    #![allow(clippy::all)]
    /// A `Lens<S, A>` focuses on a part `A` within a whole `S`.
    ///
    /// A lens is a first-class getter/setter pair that composes.
    /// In OCaml, a lens is a record with `get` and `set` fields.
    /// In Rust, we model it as a struct holding two boxed closures.
    ///
    /// - `get`: extracts the focused value from the whole
    /// - `set`: replaces the focused value, returning a new whole
    pub struct Lens<S, A> {
        getter: Getter<S, A>,
        setter: Setter<S, A>,
    }
    
    type Getter<S, A> = Box<dyn Fn(&S) -> A>;
    type Setter<S, A> = Box<dyn Fn(A, &S) -> S>;
    
    impl<S: 'static, A: 'static> Lens<S, A> {
        /// Create a lens from a getter and setter function.
        pub fn new(getter: impl Fn(&S) -> A + 'static, setter: impl Fn(A, &S) -> S + 'static) -> Self {
            Lens {
                getter: Box::new(getter),
                setter: Box::new(setter),
            }
        }
    
        /// Get the focused value from the whole.
        pub fn get(&self, s: &S) -> A {
            (self.getter)(s)
        }
    
        /// Set the focused value, returning a new whole (immutable update).
        pub fn set(&self, a: A, s: &S) -> S {
            (self.setter)(a, s)
        }
    
        /// Apply a function to the focused value — the `over` combinator.
        ///
        /// OCaml: `let over lens f s = lens.set (f (lens.get s)) s`
        /// This is the functional equivalent of "modify in place".
        pub fn over(&self, f: impl Fn(A) -> A, s: &S) -> S {
            let a = self.get(s);
            self.set(f(a), s)
        }
    
        /// Compose two lenses: `self` focuses on `A` inside `S`,
        /// `inner` focuses on `B` inside `A`.
        /// The result focuses on `B` inside `S`.
        ///
        /// OCaml:
        /// ```text
        /// 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);
        /// }
        /// ```
        pub fn compose<B: 'static>(self, inner: Lens<A, B>) -> Lens<S, B>
        where
            A: Clone,
        {
            // Move both lenses into Rc so the closures can share them
            use std::rc::Rc;
            let outer_get = Rc::new(self.getter);
            let outer_set = Rc::new(self.setter);
            let inner_get = Rc::new(inner.getter);
            let inner_set = Rc::new(inner.setter);
    
            let og = Rc::clone(&outer_get);
            let ig = Rc::clone(&inner_get);
            let composed_get = move |s: &S| -> B { ig(&og(s)) };
    
            let og2 = Rc::clone(&outer_get);
            let os2 = Rc::clone(&outer_set);
            let is2 = Rc::clone(&inner_set);
            let composed_set = move |b: B, s: &S| -> S {
                let a = og2(s);
                let a_new = is2(b, &a);
                os2(a_new, s)
            };
    
            Lens::new(composed_get, composed_set)
        }
    }
    
    // ---------------------------------------------------------------------------
    // Domain types — mirrors the OCaml example
    // ---------------------------------------------------------------------------
    
    #[derive(Debug, Clone, PartialEq)]
    pub struct Address {
        pub street: String,
        pub city: String,
    }
    
    #[derive(Debug, Clone, PartialEq)]
    pub struct Person {
        pub name: String,
        pub addr: Address,
    }
    
    /// Lens focusing on the `addr` field of a `Person`.
    pub fn addr_lens() -> Lens<Person, Address> {
        Lens::new(
            |p: &Person| p.addr.clone(), // clone needed: we return an owned Address
            |a, p| Person {
                name: p.name.clone(), // immutable update: rebuild with new addr
                addr: a,
            },
        )
    }
    
    /// Lens focusing on the `city` field of an `Address`.
    pub fn city_lens() -> Lens<Address, String> {
        Lens::new(
            |a: &Address| a.city.clone(),
            |c, a| Address {
                street: a.street.clone(),
                city: c,
            },
        )
    }
    
    /// Lens focusing on the `street` field of an `Address`.
    pub fn street_lens() -> Lens<Address, String> {
        Lens::new(
            |a: &Address| a.street.clone(),
            |s, a| Address {
                street: s,
                city: a.city.clone(),
            },
        )
    }
    
    /// Lens focusing on the `name` field of a `Person`.
    pub fn name_lens() -> Lens<Person, String> {
        Lens::new(
            |p: &Person| p.name.clone(),
            |n, p| Person {
                name: n,
                addr: p.addr.clone(),
            },
        )
    }
    
    /// Composed lens: Person → Address → city (String).
    pub fn person_city_lens() -> Lens<Person, String> {
        addr_lens().compose(city_lens())
    }
    
    /// Composed lens: Person → Address → street (String).
    pub fn person_street_lens() -> Lens<Person, String> {
        addr_lens().compose(street_lens())
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        fn sample_person() -> Person {
            Person {
                name: "Alice".to_string(),
                addr: Address {
                    street: "Main St".to_string(),
                    city: "NYC".to_string(),
                },
            }
        }
    
        // -- Basic lens get/set --
    
        #[test]
        fn test_get_city() {
            let p = sample_person();
            let lens = city_lens();
            let addr = &p.addr;
            assert_eq!(lens.get(addr), "NYC");
        }
    
        #[test]
        fn test_set_city() {
            let p = sample_person();
            let lens = city_lens();
            let new_addr = lens.set("LA".to_string(), &p.addr);
            assert_eq!(new_addr.city, "LA");
            // Original unchanged (immutable update)
            assert_eq!(p.addr.city, "NYC");
        }
    
        #[test]
        fn test_get_addr() {
            let p = sample_person();
            let lens = addr_lens();
            let addr = lens.get(&p);
            assert_eq!(addr.city, "NYC");
            assert_eq!(addr.street, "Main St");
        }
    
        #[test]
        fn test_set_addr() {
            let p = sample_person();
            let lens = addr_lens();
            let new_addr = Address {
                street: "Broadway".to_string(),
                city: "SF".to_string(),
            };
            let p2 = lens.set(new_addr, &p);
            assert_eq!(p2.addr.city, "SF");
            assert_eq!(p2.name, "Alice");
        }
    
        // -- Composed lens --
    
        #[test]
        fn test_composed_get() {
            let p = sample_person();
            let lens = person_city_lens();
            assert_eq!(lens.get(&p), "NYC");
        }
    
        #[test]
        fn test_composed_set() {
            let p = sample_person();
            let lens = person_city_lens();
            let p2 = lens.set("Boston".to_string(), &p);
            assert_eq!(lens.get(&p2), "Boston");
            // Name and street are preserved
            assert_eq!(p2.name, "Alice");
            assert_eq!(p2.addr.street, "Main St");
        }
    
        // -- Over combinator --
    
        #[test]
        fn test_over_uppercase() {
            let p = sample_person();
            let lens = person_city_lens();
            let p2 = lens.over(|c| c.to_uppercase(), &p);
            assert_eq!(lens.get(&p2), "NYC"); // "NYC" is already uppercase
        }
    
        #[test]
        fn test_over_transforms() {
            let p = Person {
                name: "Bob".to_string(),
                addr: Address {
                    street: "Elm St".to_string(),
                    city: "london".to_string(),
                },
            };
            let lens = person_city_lens();
            let p2 = lens.over(|c| c.to_uppercase(), &p);
            assert_eq!(lens.get(&p2), "LONDON");
            assert_eq!(p2.name, "Bob");
        }
    
        // -- Immutability / original unchanged --
    
        #[test]
        fn test_immutability_preserved() {
            let p = sample_person();
            let lens = person_city_lens();
            let _p2 = lens.set("Chicago".to_string(), &p);
            // Original person is unchanged
            assert_eq!(lens.get(&p), "NYC");
        }
    
        // -- Lens laws --
    
        #[test]
        fn test_get_set_law() {
            // get-set: setting what you got changes nothing
            let p = sample_person();
            let lens = person_city_lens();
            let city = lens.get(&p);
            let p2 = lens.set(city, &p);
            assert_eq!(p, p2);
        }
    
        #[test]
        fn test_set_get_law() {
            // set-get: getting what you set yields the set value
            let p = sample_person();
            let lens = person_city_lens();
            let p2 = lens.set("Denver".to_string(), &p);
            assert_eq!(lens.get(&p2), "Denver");
        }
    
        #[test]
        fn test_set_set_law() {
            // set-set: setting twice is the same as setting once with the last value
            let p = sample_person();
            let lens = person_city_lens();
            let p2 = lens.set("A".to_string(), &p);
            let p3 = lens.set("B".to_string(), &p2);
            let p4 = lens.set("B".to_string(), &p);
            assert_eq!(p3, p4);
        }
    
        // -- Additional composed lenses --
    
        #[test]
        fn test_person_street_lens() {
            let p = sample_person();
            let lens = person_street_lens();
            assert_eq!(lens.get(&p), "Main St");
            let p2 = lens.set("Oak Ave".to_string(), &p);
            assert_eq!(lens.get(&p2), "Oak Ave");
            assert_eq!(p2.addr.city, "NYC"); // city preserved
        }
    
        #[test]
        fn test_name_lens() {
            let p = sample_person();
            let lens = name_lens();
            assert_eq!(lens.get(&p), "Alice");
            let p2 = lens.set("Bob".to_string(), &p);
            assert_eq!(lens.get(&p2), "Bob");
            assert_eq!(p2.addr.city, "NYC"); // addr preserved
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        fn sample_person() -> Person {
            Person {
                name: "Alice".to_string(),
                addr: Address {
                    street: "Main St".to_string(),
                    city: "NYC".to_string(),
                },
            }
        }
    
        // -- Basic lens get/set --
    
        #[test]
        fn test_get_city() {
            let p = sample_person();
            let lens = city_lens();
            let addr = &p.addr;
            assert_eq!(lens.get(addr), "NYC");
        }
    
        #[test]
        fn test_set_city() {
            let p = sample_person();
            let lens = city_lens();
            let new_addr = lens.set("LA".to_string(), &p.addr);
            assert_eq!(new_addr.city, "LA");
            // Original unchanged (immutable update)
            assert_eq!(p.addr.city, "NYC");
        }
    
        #[test]
        fn test_get_addr() {
            let p = sample_person();
            let lens = addr_lens();
            let addr = lens.get(&p);
            assert_eq!(addr.city, "NYC");
            assert_eq!(addr.street, "Main St");
        }
    
        #[test]
        fn test_set_addr() {
            let p = sample_person();
            let lens = addr_lens();
            let new_addr = Address {
                street: "Broadway".to_string(),
                city: "SF".to_string(),
            };
            let p2 = lens.set(new_addr, &p);
            assert_eq!(p2.addr.city, "SF");
            assert_eq!(p2.name, "Alice");
        }
    
        // -- Composed lens --
    
        #[test]
        fn test_composed_get() {
            let p = sample_person();
            let lens = person_city_lens();
            assert_eq!(lens.get(&p), "NYC");
        }
    
        #[test]
        fn test_composed_set() {
            let p = sample_person();
            let lens = person_city_lens();
            let p2 = lens.set("Boston".to_string(), &p);
            assert_eq!(lens.get(&p2), "Boston");
            // Name and street are preserved
            assert_eq!(p2.name, "Alice");
            assert_eq!(p2.addr.street, "Main St");
        }
    
        // -- Over combinator --
    
        #[test]
        fn test_over_uppercase() {
            let p = sample_person();
            let lens = person_city_lens();
            let p2 = lens.over(|c| c.to_uppercase(), &p);
            assert_eq!(lens.get(&p2), "NYC"); // "NYC" is already uppercase
        }
    
        #[test]
        fn test_over_transforms() {
            let p = Person {
                name: "Bob".to_string(),
                addr: Address {
                    street: "Elm St".to_string(),
                    city: "london".to_string(),
                },
            };
            let lens = person_city_lens();
            let p2 = lens.over(|c| c.to_uppercase(), &p);
            assert_eq!(lens.get(&p2), "LONDON");
            assert_eq!(p2.name, "Bob");
        }
    
        // -- Immutability / original unchanged --
    
        #[test]
        fn test_immutability_preserved() {
            let p = sample_person();
            let lens = person_city_lens();
            let _p2 = lens.set("Chicago".to_string(), &p);
            // Original person is unchanged
            assert_eq!(lens.get(&p), "NYC");
        }
    
        // -- Lens laws --
    
        #[test]
        fn test_get_set_law() {
            // get-set: setting what you got changes nothing
            let p = sample_person();
            let lens = person_city_lens();
            let city = lens.get(&p);
            let p2 = lens.set(city, &p);
            assert_eq!(p, p2);
        }
    
        #[test]
        fn test_set_get_law() {
            // set-get: getting what you set yields the set value
            let p = sample_person();
            let lens = person_city_lens();
            let p2 = lens.set("Denver".to_string(), &p);
            assert_eq!(lens.get(&p2), "Denver");
        }
    
        #[test]
        fn test_set_set_law() {
            // set-set: setting twice is the same as setting once with the last value
            let p = sample_person();
            let lens = person_city_lens();
            let p2 = lens.set("A".to_string(), &p);
            let p3 = lens.set("B".to_string(), &p2);
            let p4 = lens.set("B".to_string(), &p);
            assert_eq!(p3, p4);
        }
    
        // -- Additional composed lenses --
    
        #[test]
        fn test_person_street_lens() {
            let p = sample_person();
            let lens = person_street_lens();
            assert_eq!(lens.get(&p), "Main St");
            let p2 = lens.set("Oak Ave".to_string(), &p);
            assert_eq!(lens.get(&p2), "Oak Ave");
            assert_eq!(p2.addr.city, "NYC"); // city preserved
        }
    
        #[test]
        fn test_name_lens() {
            let p = sample_person();
            let lens = name_lens();
            assert_eq!(lens.get(&p), "Alice");
            let p2 = lens.set("Bob".to_string(), &p);
            assert_eq!(lens.get(&p2), "Bob");
            assert_eq!(p2.addr.city, "NYC"); // addr preserved
        }
    }

    Deep Comparison

    OCaml vs Rust: Lenses — Functional Getters and Setters

    Side-by-Side Code

    OCaml

    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
    

    Rust (idiomatic — struct with boxed closures)

    pub struct Lens<S, A> {
        getter: Box<dyn Fn(&S) -> A>,
        setter: Box<dyn Fn(A, &S) -> S>,
    }
    
    impl<S: 'static, A: 'static> Lens<S, A> {
        pub fn new(
            getter: impl Fn(&S) -> A + 'static,
            setter: impl Fn(A, &S) -> S + 'static,
        ) -> Self {
            Lens { getter: Box::new(getter), setter: Box::new(setter) }
        }
    
        pub fn get(&self, s: &S) -> A { (self.getter)(s) }
        pub fn set(&self, a: A, s: &S) -> S { (self.setter)(a, s) }
        pub fn over(&self, f: impl Fn(A) -> A, s: &S) -> S {
            self.set(f(self.get(s)), s)
        }
    
        pub fn compose<B: 'static>(self, inner: Lens<A, B>) -> Lens<S, B>
        where A: Clone {
            use std::rc::Rc;
            let og = Rc::new(self.getter);
            let os = Rc::new(self.setter);
            let ig = Rc::new(inner.getter);
            let is_ = Rc::new(inner.setter);
    
            let (og1, ig1) = (Rc::clone(&og), Rc::clone(&ig));
            let (og2, os2, is2) = (Rc::clone(&og), Rc::clone(&os), Rc::clone(&is_));
    
            Lens::new(
                move |s: &S| ig1(&og1(s)),
                move |b, s: &S| { let a = og2(s); os2(is2(b, &a), s) },
            )
        }
    }
    

    Rust (functional/recursive style — lens combinators)

    fn addr_lens() -> Lens<Person, Address> {
        Lens::new(
            |p: &Person| p.addr.clone(),
            |a, p| Person { name: p.name.clone(), addr: a },
        )
    }
    
    fn city_lens() -> Lens<Address, String> {
        Lens::new(
            |a: &Address| a.city.clone(),
            |c, a| Address { street: a.street.clone(), city: c },
        )
    }
    
    fn person_city_lens() -> Lens<Person, String> {
        addr_lens().compose(city_lens())
    }
    

    Type Signatures

    ConceptOCamlRust
    Lens type('s, 'a) lens (record)Lens<S, A> (struct with Box<dyn Fn>)
    Getterget: 's -> 'aFn(&S) -> A
    Setterset: 'a -> 's -> 'sFn(A, &S) -> S
    Compose('s, 'a) lens -> ('a, 'b) lens -> ('s, 'b) lensLens<S,A>.compose(Lens<A,B>) -> Lens<S,B>
    Over('s, 'a) lens -> ('a -> 'a) -> 's -> 'slens.over(Fn(A)->A, &S) -> S
    Record update{ p with addr = a }Person { name: p.name.clone(), addr: a }

    Key Insights

  • First-class functions as fields: OCaml records can hold functions directly with no type-erasure cost. Rust needs Box<dyn Fn> (heap allocation + vtable dispatch) to store closures with different captured environments in the same struct.
  • Composition and ownership: In OCaml, composing lenses just creates a new record — closures are GC'd values. In Rust, compose must consume both lenses and wrap their internals in Rc so the composed getter and setter can both reference the original functions.
  • Clone at the boundary: OCaml's GC means get returns a value that shares the original's memory. Rust's getter returns an owned value, so struct fields must be .clone()d — this is the price of no-GC ownership.
  • Immutable update ergonomics: OCaml's { record with field = value } is built-in syntax. Rust has no equivalent — every "functional update" must list all fields explicitly (or derive a builder). This makes lenses even more valuable in Rust for hiding update boilerplate.
  • Lens laws hold identically: The three lens laws (get-set, set-get, set-set) are algebraic properties independent of language. Both OCaml and Rust lenses must satisfy them to be correct, and our tests verify all three.
  • When to Use Each Style

    Use the struct-with-closures approach when: you need composable, reusable lenses that can be stored, passed around, and combined at runtime — especially for deeply nested configuration or domain objects. Use direct field access when: the nesting is shallow (one level), performance is critical (avoid the clone + vtable overhead), or the code only reads/writes a single field.

    Exercises

  • Implement a zoom helper that takes a lens and a function operating on the focused value, returning an updated outer structure.
  • Create a lens for each field of a nested Config { server: Server { host: String, port: u16 }, timeout: u64 } struct and compose them to update the host in a single expression.
  • Implement an optional_lens (prism) that focuses on the Some variant of an Option field, returning None from get when the field is absent.
  • Open Source Repos