ExamplesBy LevelBy TopicLearning Paths
615 Advanced

Optics: optics intro

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Optics: optics intro" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Optics are composable data accessors originating from Haskell's lens library (Edward Kmett, 2012). Key difference from OCaml: 1. **HKT requirement**: Haskell's van Laarhoven encoding uses Functor/Applicative for optic unification requiring HKT; Rust uses explicit struct types per optic kind.

Tutorial

The Problem

Optics are composable data accessors originating from Haskell's lens library (Edward Kmett, 2012). They solve the deeply-nested update problem in immutable data: updating a field three levels deep requires rebuilding all intermediate values. Optics compose — a lens into a struct field composed with a prism for an enum variant gives a combined accessor that can get, set, and modify deeply nested optional values. The optic hierarchy includes Lens (exactly one focus), Prism (zero or one focus on enum variants), Traversal (zero or more foci), and Iso (lossless bidirectional conversion).

🎯 Learning Outcomes

  • • The specific optic demonstrated in this example and what it focuses on
  • • How to implement the optic manually using closures or structs in Rust
  • • How this optic composes with others in the hierarchy
  • • The laws the optic must satisfy for correct behavior
  • • Where optics are used: state management, config manipulation, nested data transformation
  • Code Example

    #![allow(clippy::all)]
    //! # Optics Introduction
    //! Composable accessors for nested data.
    
    pub struct Lens<S, A> {
        pub get: Box<dyn Fn(&S) -> A>,
        pub set: Box<dyn Fn(&S, A) -> S>,
    }
    
    impl<S: Clone + 'static, A: Clone + 'static> Lens<S, A> {
        pub fn new(get: impl Fn(&S) -> A + 'static, set: impl Fn(&S, A) -> S + 'static) -> Self {
            Lens {
                get: Box::new(get),
                set: Box::new(set),
            }
        }
        pub fn view(&self, s: &S) -> A {
            (self.get)(s)
        }
        pub fn over(&self, s: &S, f: impl Fn(A) -> A) -> S {
            let a = (self.get)(s);
            (self.set)(s, f(a))
        }
    }
    
    #[derive(Clone, Debug, PartialEq)]
    pub struct Person {
        pub name: String,
        pub age: u32,
    }
    
    pub fn name_lens() -> Lens<Person, String> {
        Lens::new(
            |p: &Person| p.name.clone(),
            |p: &Person, n| Person {
                name: n,
                ..p.clone()
            },
        )
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_lens() {
            let p = Person {
                name: "Alice".into(),
                age: 30,
            };
            let lens = name_lens();
            assert_eq!(lens.view(&p), "Alice");
            let p2 = lens.over(&p, |n| n.to_uppercase());
            assert_eq!(p2.name, "ALICE");
        }
    }

    Key Differences

  • HKT requirement: Haskell's van Laarhoven encoding uses Functor/Applicative for optic unification requiring HKT; Rust uses explicit struct types per optic kind.
  • Operator syntax: Haskell uses ^., .~, %~ for terse optic use; Rust uses method calls, more verbose but explicit.
  • Derive macros: lens-rs and similar crates provide derive macros for automatic lens generation; OCaml uses ppx_lens for the same.
  • Performance: Boxed closure implementations have runtime overhead; monomorphized generic versions compile to zero-cost abstractions.
  • OCaml Approach

    OCaml optics use the same record-with-function approach:

    type ('s, 'a) lens = { get: 's -> 'a; set: 's -> 'a -> 's }
    let name_lens = { get = (fun u -> u.name); set = (fun u n -> { u with name = n }) }
    let compose l1 l2 = { get = (fun s -> l2.get (l1.get s)); set = (fun s a -> l1.set s (l2.set (l1.get s) a)) }
    

    Full Source

    #![allow(clippy::all)]
    //! # Optics Introduction
    //! Composable accessors for nested data.
    
    pub struct Lens<S, A> {
        pub get: Box<dyn Fn(&S) -> A>,
        pub set: Box<dyn Fn(&S, A) -> S>,
    }
    
    impl<S: Clone + 'static, A: Clone + 'static> Lens<S, A> {
        pub fn new(get: impl Fn(&S) -> A + 'static, set: impl Fn(&S, A) -> S + 'static) -> Self {
            Lens {
                get: Box::new(get),
                set: Box::new(set),
            }
        }
        pub fn view(&self, s: &S) -> A {
            (self.get)(s)
        }
        pub fn over(&self, s: &S, f: impl Fn(A) -> A) -> S {
            let a = (self.get)(s);
            (self.set)(s, f(a))
        }
    }
    
    #[derive(Clone, Debug, PartialEq)]
    pub struct Person {
        pub name: String,
        pub age: u32,
    }
    
    pub fn name_lens() -> Lens<Person, String> {
        Lens::new(
            |p: &Person| p.name.clone(),
            |p: &Person, n| Person {
                name: n,
                ..p.clone()
            },
        )
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_lens() {
            let p = Person {
                name: "Alice".into(),
                age: 30,
            };
            let lens = name_lens();
            assert_eq!(lens.view(&p), "Alice");
            let p2 = lens.over(&p, |n| n.to_uppercase());
            assert_eq!(p2.name, "ALICE");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_lens() {
            let p = Person {
                name: "Alice".into(),
                age: 30,
            };
            let lens = name_lens();
            assert_eq!(lens.view(&p), "Alice");
            let p2 = lens.over(&p, |n| n.to_uppercase());
            assert_eq!(p2.name, "ALICE");
        }
    }

    Deep Comparison

    Optics

    Composable accessors for nested data

    Exercises

  • Lens laws: Write tests for all three lens laws: get-set (get after set returns set value), set-get (set to current value is identity), set-set (second set wins).
  • Prism laws: Write tests for prism laws: preview after review returns Some, set via review then preview round-trips.
  • Compose two levels: Create a lens for a struct field and a prism for an enum variant in that field — compose them and modify the inner value when the variant is present.
  • Open Source Repos