ExamplesBy LevelBy TopicLearning Paths
1081 Advanced

Lenses

Higher-order functions

Tutorial Video

Text description (accessibility)

This video demonstrates the "Lenses" functional Rust example. Difficulty level: Advanced. Key concepts covered: Higher-order functions. Implement lenses — composable functional getters and setters for nested record updates. Key difference from OCaml: 1. **Record update syntax:** OCaml `{ p with field = v }` is built

Tutorial

The Problem

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
  • 🦀 The Rust Way

    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.

    Code Example

    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)
    }

    Key Differences

  • Record update syntax: OCaml { p with field = v } is built-in; Rust requires manually constructing a new struct and cloning unchanged fields.
  • Closure sharing: OCaml closures are GC-managed values; Rust needs Arc to share closures between composed getter and setter.
  • Type bounds: OCaml's parametric polymorphism just works; Rust requires 'static + Clone bounds to store closures in boxes and rebuild values.
  • Lifetime threading: OCaml's GC makes composed getters trivial; Rust's composed getter must carefully thread lifetimes through two layers of boxed closures.
  • 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.

    Full Source

    #![allow(clippy::all)]
    /// 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.
    ///
    /// In OCaml, lenses are records of closures: `{ get: 's -> 'a; set: 'a -> 's -> 's }`.
    /// In Rust, we use boxed closures since closures have unique, unsized types.
    // Type aliases for lens closure types — avoids clippy::type_complexity
    type Getter<S, A> = Box<dyn Fn(&S) -> &A>;
    type Setter<S, A> = Box<dyn Fn(A, &S) -> S>;
    
    pub struct Lens<S, A> {
        // Takes a reference to the whole and returns a reference to the part
        get_fn: Getter<S, A>,
        // Takes a new part value and the whole, returns a new whole
        set_fn: Setter<S, A>,
    }
    
    impl<S: 'static, A: 'static> Lens<S, A> {
        /// Create a new lens from getter and setter functions.
        pub 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),
            }
        }
    
        /// Focus the lens: get the part from the whole.
        pub fn get<'s>(&self, whole: &'s S) -> &'s A {
            (self.get_fn)(whole)
        }
    
        /// Update the whole by replacing the focused part.
        pub fn set(&self, value: A, whole: &S) -> S {
            (self.set_fn)(value, whole)
        }
    
        /// Compose two lenses: `self` focuses on a mid-level part, `inner` focuses deeper.
        /// The result focuses from the outermost structure directly to the innermost part.
        ///
        /// OCaml: `compose outer inner = { get = fun s -> inner.get (outer.get s); ... }`
        /// Rust must clone the mid-level value for the set path because we need both
        /// the old mid-level (to pass to inner.set) and the whole (to pass to outer.set).
        pub fn compose<B: 'static>(self, inner: Lens<A, B>) -> Lens<S, B>
        where
            S: Clone,
            A: Clone,
        {
            // Share the inner lens between get and set closures
            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| {
                    // SAFETY of lifetime: we return a reference into `s` via two dereferences.
                    // The inner reference is valid as long as `s` is valid because both get_fns
                    // return references into their argument.
                    let mid: &A = (get_outer)(s);
                    // We need to extend the lifetime — the borrow checker can't see through
                    // the boxed closure that the returned &B borrows from s.
                    // This is safe because both get functions return references into their input.
                    let mid_ptr: *const A = mid;
                    (get_inner.get_fn)(unsafe { &*mid_ptr })
                },
                move |b: B, s: &S| {
                    // Get the current mid-level value, clone it so we can modify it
                    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.
    ///
    /// OCaml: `let over lens f s = lens.set (f (lens.get s)) s`
    pub 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(); // clone the part so we can transform it
        lens.set(f(current), whole)
    }
    
    // --- Domain types for demonstration ---
    
    #[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,
            |a: Address, p: &Person| Person {
                name: p.name.clone(), // clone name — we're building a new Person
                addr: a,
            },
        )
    }
    
    /// Lens focusing on the `city` field of an `Address`.
    pub fn city_lens() -> Lens<Address, String> {
        Lens::new(
            |a: &Address| &a.city,
            |c: String, a: &Address| Address {
                street: a.street.clone(), // clone street — we're building a new Address
                city: c,
            },
        )
    }
    
    /// Lens focusing on the `street` field of an `Address`.
    pub fn street_lens() -> Lens<Address, String> {
        Lens::new(
            |a: &Address| &a.street,
            |s: String, a: &Address| Address {
                street: s,
                city: a.city.clone(), // clone city — we're building a new Address
            },
        )
    }
    
    /// Composed lens: Person -> city (via addr).
    pub fn person_city_lens() -> Lens<Person, String> {
        addr_lens().compose(city_lens())
    }
    
    /// Composed lens: Person -> street (via addr).
    pub fn person_street_lens() -> Lens<Person, String> {
        addr_lens().compose(street_lens())
    }
    
    pub fn sample_person() -> Person {
        Person {
            name: "Alice".to_string(),
            addr: Address {
                street: "Main St".to_string(),
                city: "NYC".to_string(),
            },
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_simple_get() {
            let p = sample_person();
            let lens = addr_lens();
            assert_eq!(lens.get(&p).city, "NYC");
        }
    
        #[test]
        fn test_simple_set() {
            let p = sample_person();
            let lens = city_lens();
            let new_addr = lens.set("LA".to_string(), &p.addr);
            assert_eq!(new_addr.city, "LA");
            assert_eq!(new_addr.street, "Main St");
        }
    
        #[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!(p2.addr.city, "Boston");
            assert_eq!(p2.name, "Alice");
            assert_eq!(p2.addr.street, "Main St");
        }
    
        #[test]
        fn test_over_transforms_focused_value() {
            let p = sample_person();
            let lens = person_city_lens();
            let p2 = over(&lens, |c| c.to_lowercase(), &p);
            assert_eq!(p2.addr.city, "nyc");
            assert_eq!(p2.name, "Alice");
        }
    
        #[test]
        fn test_original_unchanged_after_set() {
            let p = sample_person();
            let lens = person_city_lens();
            let _p2 = lens.set("Boston".to_string(), &p);
            // Original is unchanged — functional update
            assert_eq!(p.addr.city, "NYC");
        }
    
        #[test]
        fn test_set_get_roundtrip() {
            let p = sample_person();
            let lens = person_city_lens();
            let p2 = lens.set("SF".to_string(), &p);
            assert_eq!(lens.get(&p2), "SF");
        }
    
        #[test]
        fn test_compose_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!(p2.addr.street, "Oak Ave");
            assert_eq!(p2.addr.city, "NYC");
        }
    
        #[test]
        fn test_over_with_street() {
            let p = sample_person();
            let lens = person_street_lens();
            let p2 = over(&lens, |s| format!("123 {s}"), &p);
            assert_eq!(lens.get(&p2), "123 Main St");
        }
    
        #[test]
        fn test_lens_laws_get_set() {
            // Law: set (get s) s == s
            let p = sample_person();
            let lens = person_city_lens();
            let city = lens.get(&p).clone();
            let p2 = lens.set(city, &p);
            assert_eq!(p, p2);
        }
    
        #[test]
        fn test_lens_laws_set_get() {
            // Law: get (set a s) == a
            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_lens_laws_set_set() {
            // Law: set b (set a s) == set b s
            let p = sample_person();
            let lens = person_city_lens();
            let p_ab = lens.set("B".to_string(), &lens.set("A".to_string(), &p));
            let p_b = lens.set("B".to_string(), &p);
            assert_eq!(p_ab, p_b);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_simple_get() {
            let p = sample_person();
            let lens = addr_lens();
            assert_eq!(lens.get(&p).city, "NYC");
        }
    
        #[test]
        fn test_simple_set() {
            let p = sample_person();
            let lens = city_lens();
            let new_addr = lens.set("LA".to_string(), &p.addr);
            assert_eq!(new_addr.city, "LA");
            assert_eq!(new_addr.street, "Main St");
        }
    
        #[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!(p2.addr.city, "Boston");
            assert_eq!(p2.name, "Alice");
            assert_eq!(p2.addr.street, "Main St");
        }
    
        #[test]
        fn test_over_transforms_focused_value() {
            let p = sample_person();
            let lens = person_city_lens();
            let p2 = over(&lens, |c| c.to_lowercase(), &p);
            assert_eq!(p2.addr.city, "nyc");
            assert_eq!(p2.name, "Alice");
        }
    
        #[test]
        fn test_original_unchanged_after_set() {
            let p = sample_person();
            let lens = person_city_lens();
            let _p2 = lens.set("Boston".to_string(), &p);
            // Original is unchanged — functional update
            assert_eq!(p.addr.city, "NYC");
        }
    
        #[test]
        fn test_set_get_roundtrip() {
            let p = sample_person();
            let lens = person_city_lens();
            let p2 = lens.set("SF".to_string(), &p);
            assert_eq!(lens.get(&p2), "SF");
        }
    
        #[test]
        fn test_compose_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!(p2.addr.street, "Oak Ave");
            assert_eq!(p2.addr.city, "NYC");
        }
    
        #[test]
        fn test_over_with_street() {
            let p = sample_person();
            let lens = person_street_lens();
            let p2 = over(&lens, |s| format!("123 {s}"), &p);
            assert_eq!(lens.get(&p2), "123 Main St");
        }
    
        #[test]
        fn test_lens_laws_get_set() {
            // Law: set (get s) s == s
            let p = sample_person();
            let lens = person_city_lens();
            let city = lens.get(&p).clone();
            let p2 = lens.set(city, &p);
            assert_eq!(p, p2);
        }
    
        #[test]
        fn test_lens_laws_set_get() {
            // Law: get (set a s) == a
            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_lens_laws_set_set() {
            // Law: set b (set a s) == set b s
            let p = sample_person();
            let lens = person_city_lens();
            let p_ab = lens.set("B".to_string(), &lens.set("A".to_string(), &p));
            let p_b = lens.set("B".to_string(), &p);
            assert_eq!(p_ab, p_b);
        }
    }

    Deep Comparison

    OCaml vs Rust: Lenses

    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
    
    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)

    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

    ConceptOCamlRust
    Lens type('s, 'a) lensLens<S, A>
    Get's -> 'aFn(&S) -> &A
    Set'a -> 's -> 'sFn(A, &S) -> S
    Compose('s, 'a) lens -> ('a, 'b) lens -> ('s, 'b) lensLens<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

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.

    Exercises

  • Implement a modify combinator for lenses that takes a lens and a function f: A -> A and returns a new value with the focused field updated in-place.
  • Compose two lenses to focus on a nested field three levels deep, and use the composed lens to both read and update the value.
  • Implement a traversal — a lens-like abstraction that focuses on multiple elements simultaneously (e.g., all items in a Vec) — and use it to uppercase all strings in a nested data structure.
  • Open Source Repos