ExamplesBy LevelBy TopicLearning Paths
202 Advanced

Lens Basics — Get and Set

Functional Programming

Tutorial

The Problem

A lens is formally a pair of functions: get: S -> A (extract a field of type A from a structure S) and set: A -> S -> S (return a new S with the field replaced). This simple representation enables all lens operations: view (= get), set, over (apply function to field), and composition. Understanding the basic Lens<S, A> struct and its derived operations is the prerequisite for all optics concepts that follow.

🎯 Learning Outcomes

  • • Implement a Lens<S, A> struct with get and set functions
  • • Derive view, set, and over from the basic lens definition
  • • Create lenses for specific record fields by providing concrete get/set pairs
  • • Understand that a lens must satisfy three laws (covered in detail in example 203)
  • Code Example

    struct Lens<S, A> {
        get: Box<dyn Fn(&S) -> A>,
        set: Box<dyn Fn(A, &S) -> S>,
    }
    
    impl<S: 'static, A: 'static> Lens<S, A> {
        fn view(&self, s: &S) -> A { (self.get)(s) }
        fn set(&self, a: A, s: &S) -> S { (self.set)(a, s) }
        fn over(&self, f: impl FnOnce(A) -> A, s: &S) -> S {
            (self.set)(f((self.get)(s)), s)
        }
    }

    Key Differences

  • Struct vs. record: OCaml uses records; Rust uses structs — structurally identical, syntactically different.
  • Box<dyn Fn> overhead: Rust's Box<dyn Fn> adds heap allocation; OCaml's function fields are heap-allocated via GC — equivalent overhead.
  • Derive macros: Production Rust code uses #[derive(Lens)] from the lens-rs crate; OCaml's ppx_lens generates the same.
  • Immutable update: Both set functions return a new structure without modifying the original — pure functional update.
  • OCaml Approach

    OCaml lenses are typically records:

    type ('s, 'a) lens = {
      get : 's -> 'a;
      set : 'a -> 's -> 's;
    }
    let view l s = l.get s
    let set l a s = l.set a s
    let over l f s = l.set (f (l.get s)) s
    

    OCaml's record syntax makes the definition clean. Haskell's lens package uses a different (Van Laarhoven) encoding for better composition, but the record encoding is clearer for learning.

    Full Source

    #![allow(clippy::all)]
    // Example 202: Lens Basics — Lens as a Pair of Get and Set
    
    // === Approach 1: Lens as struct with closures === //
    
    struct Lens<S, A> {
        get: Box<dyn Fn(&S) -> A>,
        set: Box<dyn Fn(A, &S) -> S>,
    }
    
    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: Box::new(get),
                set: Box::new(set),
            }
        }
    
        fn view(&self, s: &S) -> A {
            (self.get)(s)
        }
    
        fn set(&self, a: A, s: &S) -> S {
            (self.set)(a, s)
        }
    
        fn over(&self, f: impl FnOnce(A) -> A, s: &S) -> S {
            let a = (self.get)(s);
            (self.set)(f(a), s)
        }
    }
    
    // === Approach 2: Lens via trait (zero-cost abstraction) === //
    
    trait LensLike<S, A> {
        fn get(s: &S) -> A;
        fn set(a: A, s: &S) -> S;
    
        fn over(f: impl FnOnce(A) -> A, s: &S) -> S {
            let a = Self::get(s);
            Self::set(f(a), s)
        }
    }
    
    // === Approach 3: Macro-generated lenses === //
    
    macro_rules! make_lens {
        ($lens_name:ident, $struct:ty, $field:ident, $field_ty:ty) => {
            struct $lens_name;
            impl LensLike<$struct, $field_ty> for $lens_name {
                fn get(s: &$struct) -> $field_ty {
                    s.$field.clone()
                }
                fn set(a: $field_ty, s: &$struct) -> $struct {
                    let mut new = s.clone();
                    new.$field = a;
                    new
                }
            }
        };
    }
    
    #[derive(Debug, Clone, PartialEq)]
    struct Person {
        name: String,
        age: u32,
    }
    
    #[derive(Debug, Clone, PartialEq)]
    struct Address {
        street: String,
        city: String,
        zip: String,
    }
    
    #[derive(Debug, Clone, PartialEq)]
    struct Employee {
        emp_name: String,
        address: Address,
    }
    
    // Generate lenses via macro
    make_lens!(PersonName, Person, name, String);
    make_lens!(PersonAge, Person, age, u32);
    make_lens!(EmpAddress, Employee, address, Address);
    make_lens!(AddrCity, Address, city, String);
    
    fn name_lens() -> Lens<Person, String> {
        Lens::new(
            |p: &Person| p.name.clone(),
            |n: String, p: &Person| Person {
                name: n,
                ..p.clone()
            },
        )
    }
    
    fn age_lens() -> Lens<Person, u32> {
        Lens::new(
            |p: &Person| p.age,
            |a: u32, p: &Person| Person {
                age: a,
                ..p.clone()
            },
        )
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_closure_lens_get_set() {
            let p = Person {
                name: "Bob".into(),
                age: 25,
            };
            let nl = name_lens();
            assert_eq!(nl.view(&p), "Bob");
            let p2 = nl.set("Robert".into(), &p);
            assert_eq!(nl.view(&p2), "Robert");
            assert_eq!(p2.age, 25); // other fields unchanged
        }
    
        #[test]
        fn test_trait_lens() {
            let p = Person {
                name: "Eve".into(),
                age: 40,
            };
            assert_eq!(PersonAge::get(&p), 40);
            let p2 = PersonAge::set(41, &p);
            assert_eq!(PersonAge::get(&p2), 41);
        }
    
        #[test]
        fn test_over_modify() {
            let p = Person {
                name: "X".into(),
                age: 10,
            };
            let p2 = PersonAge::over(|a| a * 2, &p);
            assert_eq!(PersonAge::get(&p2), 20);
        }
    
        #[test]
        fn test_macro_lens_for_nested() {
            let emp = Employee {
                emp_name: "Charlie".into(),
                address: Address {
                    street: "1st".into(),
                    city: "NYC".into(),
                    zip: "10001".into(),
                },
            };
            let addr = EmpAddress::get(&emp);
            assert_eq!(AddrCity::get(&addr), "NYC");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_closure_lens_get_set() {
            let p = Person {
                name: "Bob".into(),
                age: 25,
            };
            let nl = name_lens();
            assert_eq!(nl.view(&p), "Bob");
            let p2 = nl.set("Robert".into(), &p);
            assert_eq!(nl.view(&p2), "Robert");
            assert_eq!(p2.age, 25); // other fields unchanged
        }
    
        #[test]
        fn test_trait_lens() {
            let p = Person {
                name: "Eve".into(),
                age: 40,
            };
            assert_eq!(PersonAge::get(&p), 40);
            let p2 = PersonAge::set(41, &p);
            assert_eq!(PersonAge::get(&p2), 41);
        }
    
        #[test]
        fn test_over_modify() {
            let p = Person {
                name: "X".into(),
                age: 10,
            };
            let p2 = PersonAge::over(|a| a * 2, &p);
            assert_eq!(PersonAge::get(&p2), 20);
        }
    
        #[test]
        fn test_macro_lens_for_nested() {
            let emp = Employee {
                emp_name: "Charlie".into(),
                address: Address {
                    street: "1st".into(),
                    city: "NYC".into(),
                    zip: "10001".into(),
                },
            };
            let addr = EmpAddress::get(&emp);
            assert_eq!(AddrCity::get(&addr), "NYC");
        }
    }

    Deep Comparison

    Comparison: Example 202 — Lens Basics

    Lens Type Definition

    OCaml

    type ('s, 'a) lens = {
      get : 's -> 'a;
      set : 'a -> 's -> 's;
    }
    
    let view l s = l.get s
    let set l a s = l.set a s
    let over l f s = l.set (f (l.get s)) s
    

    Rust

    struct Lens<S, A> {
        get: Box<dyn Fn(&S) -> A>,
        set: Box<dyn Fn(A, &S) -> S>,
    }
    
    impl<S: 'static, A: 'static> Lens<S, A> {
        fn view(&self, s: &S) -> A { (self.get)(s) }
        fn set(&self, a: A, s: &S) -> S { (self.set)(a, s) }
        fn over(&self, f: impl FnOnce(A) -> A, s: &S) -> S {
            (self.set)(f((self.get)(s)), s)
        }
    }
    

    Creating a Lens

    OCaml

    let name_lens = {
      get = (fun p -> p.name);
      set = (fun n p -> { p with name = n });
    }
    

    Rust (Closure)

    fn name_lens() -> Lens<Person, String> {
        Lens::new(|p| p.name.clone(), |n, p| Person { name: n, ..p.clone() })
    }
    

    Rust (Trait — zero-cost)

    struct PersonName;
    impl LensLike<Person, String> for PersonName {
        fn get(s: &Person) -> String { s.name.clone() }
        fn set(a: String, s: &Person) -> Person {
            Person { name: a, ..s.clone() }
        }
    }
    

    Exercises

  • Create lenses for Circle { center: Point, radius: f64 } — one for center and one for radius.
  • Implement lens_compose(outer: Lens<A, B>, inner: Lens<B, C>) -> Lens<A, C> that composes two lenses.
  • Write view_opt: Lens<S, Option<A>> -> S -> Option<A> that handles optional fields.
  • Open Source Repos