ExamplesBy LevelBy TopicLearning Paths
209 Advanced

Affine Traversal — At Most One Focus

Functional Programming

Tutorial

The Problem

Between a lens (exactly one focus) and a traversal (zero or more focuses) lies the affine traversal (at most one focus). It is the combination of a prism (might not exist) and a lens (focused access). HashMap::get is an affine traversal: it focuses on the value at a key if it exists, with no-op updates when the key is absent. Optional record fields and possibly-missing array elements are natural affine traversals.

🎯 Learning Outcomes

  • • Understand affine traversals as the intersection of prisms and lenses
  • • Implement preview (extract if present) and over (modify if present) for affine traversals
  • • See HashMap lookups as canonical examples of affine traversal
  • • Understand where affine traversals fit in the optics hierarchy (between prism and traversal)
  • Code Example

    pub struct Affine<S, A> {
        preview: Box<dyn Fn(&S) -> Option<A>>,
        set: Box<dyn Fn(A, &S) -> S>,
    }
    
    impl<S: Clone + 'static, A: 'static> Affine<S, A> {
        pub fn over(&self, f: impl FnOnce(A) -> A, s: &S) -> S {
            match (self.preview)(s) {
                Some(v) => (self.set)(f(v), s),
                None    => s.clone(),
            }
        }
    }
    
    pub fn email_affine() -> Affine<User, String> {
        Affine::new(
            |u: &User| u.email.clone(),
            |e, u: &User| User { email: Some(e), ..u.clone() },
        )
    }

    Key Differences

  • Naming: Rust and Haskell call it "affine traversal"; OCaml's optics library calls it "optional"; the semantics are the same.
  • HashMap focus: Rust's HashMap operations naturally implement the affine traversal interface; OCaml's Map.S.find_opt similarly provides affine access.
  • Composition: Composing a lens with an affine traversal gives an affine traversal; composing two affine traversals gives an affine traversal — the class is closed under composition.
  • Practical use: Affine traversals are the most practically used optic after lenses — optional field access is ubiquitous.
  • OCaml Approach

    OCaml's affine traversal is called an "optional" in the optics literature. The optics library provides Optional.t with getOption and set operations. OCaml's Map.find_opt : key -> 'a Map.t -> 'a option is the standard affine traversal for ordered maps. The composition of a lens into an option field produces an affine traversal automatically.

    Full Source

    #![allow(clippy::all)]
    // Example 209: Affine Traversal — At Most One Focus
    //
    // An affine traversal focuses on at most one value: `preview` returns
    // `Option<A>`, and `over`/`set` are no-ops when the target is absent.
    //
    // It combines the "might not exist" of a Prism with the "exactly one"
    // of a Lens.  Typical use cases: optional record fields, HashMap lookups,
    // the head of a possibly-empty Vec.
    
    use std::collections::HashMap;
    
    // ---------------------------------------------------------------------------
    // Core Affine struct
    // ---------------------------------------------------------------------------
    
    type PreviewFn<S, A> = Box<dyn Fn(&S) -> Option<A>>;
    type SetFn<S, A> = Box<dyn Fn(A, &S) -> S>;
    
    /// An affine traversal: `preview` extracts at most one value; `set` replaces
    /// it when present and is a structural no-op otherwise.
    pub struct Affine<S, A> {
        preview: PreviewFn<S, A>,
        set: SetFn<S, A>,
    }
    
    impl<S: Clone + 'static, A: 'static> Affine<S, A> {
        pub fn new(
            preview: impl Fn(&S) -> Option<A> + 'static,
            set: impl Fn(A, &S) -> S + 'static,
        ) -> Self {
            Affine {
                preview: Box::new(preview),
                set: Box::new(set),
            }
        }
    
        /// Extract the focus value, returning `None` if absent.
        pub fn preview(&self, s: &S) -> Option<A> {
            (self.preview)(s)
        }
    
        /// Replace the focus value; if absent, returns a clone of `s` unchanged.
        pub fn set(&self, a: A, s: &S) -> S {
            (self.set)(a, s)
        }
    
        /// Apply `f` to the focus value; if absent, returns a clone of `s`.
        pub fn over(&self, f: impl FnOnce(A) -> A, s: &S) -> S {
            match (self.preview)(s) {
                Some(v) => (self.set)(f(v), s),
                None => s.clone(),
            }
        }
    }
    
    // ---------------------------------------------------------------------------
    // Approach 1: Affine for optional record fields
    // ---------------------------------------------------------------------------
    
    #[derive(Debug, Clone, PartialEq)]
    pub struct User {
        pub name: String,
        pub email: Option<String>,
        pub phone: Option<String>,
    }
    
    /// Affine traversal targeting `User::email`.
    pub fn email_affine() -> Affine<User, String> {
        Affine::new(
            |u: &User| u.email.clone(),
            |e, u: &User| User {
                email: Some(e),
                ..u.clone()
            },
        )
    }
    
    /// Affine traversal targeting `User::phone`.
    pub fn phone_affine() -> Affine<User, String> {
        Affine::new(
            |u: &User| u.phone.clone(),
            |p, u: &User| User {
                phone: Some(p),
                ..u.clone()
            },
        )
    }
    
    // ---------------------------------------------------------------------------
    // Approach 2: Affine for HashMap key lookups
    // ---------------------------------------------------------------------------
    
    /// Build an affine traversal that focuses on a specific key in a `HashMap`.
    ///
    /// The key is captured at construction time, matching the OCaml approach of
    /// returning a record with closures that close over the key.
    pub fn map_key_affine(key: &str) -> Affine<HashMap<String, String>, String> {
        let k_preview = key.to_string();
        let k_set = key.to_string();
        Affine::new(
            move |m: &HashMap<String, String>| m.get(&k_preview).cloned(),
            move |v, m: &HashMap<String, String>| {
                let mut out = m.clone();
                out.insert(k_set.clone(), v);
                out
            },
        )
    }
    
    // ---------------------------------------------------------------------------
    // Approach 3: Affine for the head of a Vec
    // ---------------------------------------------------------------------------
    
    /// Affine traversal targeting the first element of a `Vec<i32>`.
    /// `over` on an empty vec is a no-op.
    pub fn vec_head_affine() -> Affine<Vec<i32>, i32> {
        Affine::new(
            |v: &Vec<i32>| v.first().copied(),
            |x, v: &Vec<i32>| {
                if v.is_empty() {
                    v.clone()
                } else {
                    let mut out = v.clone();
                    out[0] = x;
                    out
                }
            },
        )
    }
    
    // ---------------------------------------------------------------------------
    // Approach 4: Trait-based zero-cost affine (no heap allocation)
    // ---------------------------------------------------------------------------
    
    /// Implement to get compile-time dispatch affine traversal behaviour.
    pub trait AffineTraversal {
        type Source: Clone;
        type Focus;
    
        fn preview(s: &Self::Source) -> Option<Self::Focus>;
        fn set(a: Self::Focus, s: &Self::Source) -> Self::Source;
    
        fn over(f: impl FnOnce(Self::Focus) -> Self::Focus, s: &Self::Source) -> Self::Source {
            match Self::preview(s) {
                Some(v) => Self::set(f(v), s),
                None => s.clone(),
            }
        }
    }
    
    /// Zero-cost affine for `User::email` via the trait.
    pub struct UserEmailAffine;
    
    impl AffineTraversal for UserEmailAffine {
        type Source = User;
        type Focus = String;
    
        fn preview(u: &User) -> Option<String> {
            u.email.clone()
        }
    
        fn set(e: String, u: &User) -> User {
            User {
                email: Some(e),
                ..u.clone()
            }
        }
    }
    
    // ---------------------------------------------------------------------------
    // Tests
    // ---------------------------------------------------------------------------
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // --- Approach 1: optional record fields ---
    
        #[test]
        fn test_email_preview_present() {
            let u = User {
                name: "Alice".to_string(),
                email: Some("alice@example.com".to_string()),
                phone: None,
            };
            let aff = email_affine();
            assert_eq!(aff.preview(&u), Some("alice@example.com".to_string()));
        }
    
        #[test]
        fn test_email_preview_absent() {
            let u = User {
                name: "Bob".to_string(),
                email: None,
                phone: None,
            };
            let aff = email_affine();
            assert_eq!(aff.preview(&u), None);
        }
    
        #[test]
        fn test_email_set_when_present() {
            let u = User {
                name: "Alice".to_string(),
                email: Some("old@example.com".to_string()),
                phone: None,
            };
            let aff = email_affine();
            let updated = aff.set("new@example.com".to_string(), &u);
            assert_eq!(updated.email, Some("new@example.com".to_string()));
            assert_eq!(updated.name, "Alice");
        }
    
        #[test]
        fn test_email_set_when_absent_installs_value() {
            // set always writes; the affine does not guard on prior existence
            let u = User {
                name: "Bob".to_string(),
                email: None,
                phone: None,
            };
            let aff = email_affine();
            let updated = aff.set("bob@example.com".to_string(), &u);
            assert_eq!(updated.email, Some("bob@example.com".to_string()));
        }
    
        #[test]
        fn test_over_uppercases_present_email() {
            let u = User {
                name: "Alice".to_string(),
                email: Some("alice@example.com".to_string()),
                phone: None,
            };
            let aff = email_affine();
            let updated = aff.over(|e| e.to_uppercase(), &u);
            assert_eq!(updated.email, Some("ALICE@EXAMPLE.COM".to_string()));
            // Other fields untouched
            assert_eq!(updated.name, "Alice");
            assert_eq!(updated.phone, None);
        }
    
        #[test]
        fn test_over_is_noop_when_email_absent() {
            let u = User {
                name: "Bob".to_string(),
                email: None,
                phone: None,
            };
            let aff = email_affine();
            let updated = aff.over(|e| e.to_uppercase(), &u);
            assert_eq!(updated, u); // structurally unchanged
        }
    
        #[test]
        fn test_phone_affine_independent_of_email() {
            let u = User {
                name: "Carol".to_string(),
                email: Some("carol@example.com".to_string()),
                phone: Some("555-1234".to_string()),
            };
            let phone = phone_affine();
            let updated = phone.over(|p| p.replace('-', ""), &u);
            assert_eq!(updated.phone, Some("5551234".to_string()));
            assert_eq!(updated.email, Some("carol@example.com".to_string()));
        }
    
        // --- Approach 2: HashMap key ---
    
        #[test]
        fn test_map_key_preview_present() {
            let mut m = HashMap::new();
            m.insert("lang".to_string(), "rust".to_string());
            let aff = map_key_affine("lang");
            assert_eq!(aff.preview(&m), Some("rust".to_string()));
        }
    
        #[test]
        fn test_map_key_preview_absent() {
            let m: HashMap<String, String> = HashMap::new();
            let aff = map_key_affine("missing");
            assert_eq!(aff.preview(&m), None);
        }
    
        #[test]
        fn test_map_key_over_transforms_existing_value() {
            let mut m = HashMap::new();
            m.insert("greeting".to_string(), "hello".to_string());
            let aff = map_key_affine("greeting");
            let updated = aff.over(|v| v.to_uppercase(), &m);
            assert_eq!(updated.get("greeting"), Some(&"HELLO".to_string()));
        }
    
        #[test]
        fn test_map_key_over_noop_on_missing_key() {
            let m: HashMap<String, String> = HashMap::new();
            let aff = map_key_affine("nope");
            let updated = aff.over(|v| v.to_uppercase(), &m);
            assert!(updated.is_empty());
        }
    
        // --- Approach 3: Vec head ---
    
        #[test]
        fn test_vec_head_preview_nonempty() {
            let v = vec![10, 20, 30];
            let aff = vec_head_affine();
            assert_eq!(aff.preview(&v), Some(10));
        }
    
        #[test]
        fn test_vec_head_preview_empty() {
            let v: Vec<i32> = vec![];
            let aff = vec_head_affine();
            assert_eq!(aff.preview(&v), None);
        }
    
        #[test]
        fn test_vec_head_over_doubles_head() {
            let v = vec![3, 4, 5];
            let aff = vec_head_affine();
            let updated = aff.over(|x| x * 2, &v);
            assert_eq!(updated, vec![6, 4, 5]);
        }
    
        #[test]
        fn test_vec_head_over_noop_on_empty() {
            let v: Vec<i32> = vec![];
            let aff = vec_head_affine();
            let updated = aff.over(|x| x * 2, &v);
            assert_eq!(updated, v);
        }
    
        // --- Approach 4: trait-based ---
    
        #[test]
        fn test_trait_affine_preview_present() {
            let u = User {
                name: "Dana".to_string(),
                email: Some("dana@example.com".to_string()),
                phone: None,
            };
            assert_eq!(
                UserEmailAffine::preview(&u),
                Some("dana@example.com".to_string())
            );
        }
    
        #[test]
        fn test_trait_affine_preview_absent() {
            let u = User {
                name: "Eve".to_string(),
                email: None,
                phone: None,
            };
            assert_eq!(UserEmailAffine::preview(&u), None);
        }
    
        #[test]
        fn test_trait_affine_over_present() {
            let u = User {
                name: "Dana".to_string(),
                email: Some("dana@example.com".to_string()),
                phone: None,
            };
            let updated = UserEmailAffine::over(|e| e.to_uppercase(), &u);
            assert_eq!(updated.email, Some("DANA@EXAMPLE.COM".to_string()));
        }
    
        #[test]
        fn test_trait_affine_over_absent_noop() {
            let u = User {
                name: "Eve".to_string(),
                email: None,
                phone: None,
            };
            let updated = UserEmailAffine::over(|e| e.to_uppercase(), &u);
            assert_eq!(updated, u);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // --- Approach 1: optional record fields ---
    
        #[test]
        fn test_email_preview_present() {
            let u = User {
                name: "Alice".to_string(),
                email: Some("alice@example.com".to_string()),
                phone: None,
            };
            let aff = email_affine();
            assert_eq!(aff.preview(&u), Some("alice@example.com".to_string()));
        }
    
        #[test]
        fn test_email_preview_absent() {
            let u = User {
                name: "Bob".to_string(),
                email: None,
                phone: None,
            };
            let aff = email_affine();
            assert_eq!(aff.preview(&u), None);
        }
    
        #[test]
        fn test_email_set_when_present() {
            let u = User {
                name: "Alice".to_string(),
                email: Some("old@example.com".to_string()),
                phone: None,
            };
            let aff = email_affine();
            let updated = aff.set("new@example.com".to_string(), &u);
            assert_eq!(updated.email, Some("new@example.com".to_string()));
            assert_eq!(updated.name, "Alice");
        }
    
        #[test]
        fn test_email_set_when_absent_installs_value() {
            // set always writes; the affine does not guard on prior existence
            let u = User {
                name: "Bob".to_string(),
                email: None,
                phone: None,
            };
            let aff = email_affine();
            let updated = aff.set("bob@example.com".to_string(), &u);
            assert_eq!(updated.email, Some("bob@example.com".to_string()));
        }
    
        #[test]
        fn test_over_uppercases_present_email() {
            let u = User {
                name: "Alice".to_string(),
                email: Some("alice@example.com".to_string()),
                phone: None,
            };
            let aff = email_affine();
            let updated = aff.over(|e| e.to_uppercase(), &u);
            assert_eq!(updated.email, Some("ALICE@EXAMPLE.COM".to_string()));
            // Other fields untouched
            assert_eq!(updated.name, "Alice");
            assert_eq!(updated.phone, None);
        }
    
        #[test]
        fn test_over_is_noop_when_email_absent() {
            let u = User {
                name: "Bob".to_string(),
                email: None,
                phone: None,
            };
            let aff = email_affine();
            let updated = aff.over(|e| e.to_uppercase(), &u);
            assert_eq!(updated, u); // structurally unchanged
        }
    
        #[test]
        fn test_phone_affine_independent_of_email() {
            let u = User {
                name: "Carol".to_string(),
                email: Some("carol@example.com".to_string()),
                phone: Some("555-1234".to_string()),
            };
            let phone = phone_affine();
            let updated = phone.over(|p| p.replace('-', ""), &u);
            assert_eq!(updated.phone, Some("5551234".to_string()));
            assert_eq!(updated.email, Some("carol@example.com".to_string()));
        }
    
        // --- Approach 2: HashMap key ---
    
        #[test]
        fn test_map_key_preview_present() {
            let mut m = HashMap::new();
            m.insert("lang".to_string(), "rust".to_string());
            let aff = map_key_affine("lang");
            assert_eq!(aff.preview(&m), Some("rust".to_string()));
        }
    
        #[test]
        fn test_map_key_preview_absent() {
            let m: HashMap<String, String> = HashMap::new();
            let aff = map_key_affine("missing");
            assert_eq!(aff.preview(&m), None);
        }
    
        #[test]
        fn test_map_key_over_transforms_existing_value() {
            let mut m = HashMap::new();
            m.insert("greeting".to_string(), "hello".to_string());
            let aff = map_key_affine("greeting");
            let updated = aff.over(|v| v.to_uppercase(), &m);
            assert_eq!(updated.get("greeting"), Some(&"HELLO".to_string()));
        }
    
        #[test]
        fn test_map_key_over_noop_on_missing_key() {
            let m: HashMap<String, String> = HashMap::new();
            let aff = map_key_affine("nope");
            let updated = aff.over(|v| v.to_uppercase(), &m);
            assert!(updated.is_empty());
        }
    
        // --- Approach 3: Vec head ---
    
        #[test]
        fn test_vec_head_preview_nonempty() {
            let v = vec![10, 20, 30];
            let aff = vec_head_affine();
            assert_eq!(aff.preview(&v), Some(10));
        }
    
        #[test]
        fn test_vec_head_preview_empty() {
            let v: Vec<i32> = vec![];
            let aff = vec_head_affine();
            assert_eq!(aff.preview(&v), None);
        }
    
        #[test]
        fn test_vec_head_over_doubles_head() {
            let v = vec![3, 4, 5];
            let aff = vec_head_affine();
            let updated = aff.over(|x| x * 2, &v);
            assert_eq!(updated, vec![6, 4, 5]);
        }
    
        #[test]
        fn test_vec_head_over_noop_on_empty() {
            let v: Vec<i32> = vec![];
            let aff = vec_head_affine();
            let updated = aff.over(|x| x * 2, &v);
            assert_eq!(updated, v);
        }
    
        // --- Approach 4: trait-based ---
    
        #[test]
        fn test_trait_affine_preview_present() {
            let u = User {
                name: "Dana".to_string(),
                email: Some("dana@example.com".to_string()),
                phone: None,
            };
            assert_eq!(
                UserEmailAffine::preview(&u),
                Some("dana@example.com".to_string())
            );
        }
    
        #[test]
        fn test_trait_affine_preview_absent() {
            let u = User {
                name: "Eve".to_string(),
                email: None,
                phone: None,
            };
            assert_eq!(UserEmailAffine::preview(&u), None);
        }
    
        #[test]
        fn test_trait_affine_over_present() {
            let u = User {
                name: "Dana".to_string(),
                email: Some("dana@example.com".to_string()),
                phone: None,
            };
            let updated = UserEmailAffine::over(|e| e.to_uppercase(), &u);
            assert_eq!(updated.email, Some("DANA@EXAMPLE.COM".to_string()));
        }
    
        #[test]
        fn test_trait_affine_over_absent_noop() {
            let u = User {
                name: "Eve".to_string(),
                email: None,
                phone: None,
            };
            let updated = UserEmailAffine::over(|e| e.to_uppercase(), &u);
            assert_eq!(updated, u);
        }
    }

    Deep Comparison

    OCaml vs Rust: Affine Traversal — At Most One Focus

    Side-by-Side Code

    OCaml

    type ('s, 'a) affine = {
      preview : 's -> 'a option;
      set     : 'a -> 's -> 's;
    }
    
    type user = { name: string; email: string option; phone: string option }
    
    let email_affine : (user, string) affine = {
      preview = (fun u -> u.email);
      set = (fun e u -> { u with email = Some e });
    }
    
    (* over = apply f when present, no-op when absent *)
    let over aff f s =
      match aff.preview s with
      | Some v -> aff.set (f v) s
      | None   -> s
    

    Rust (idiomatic — boxed closures)

    pub struct Affine<S, A> {
        preview: Box<dyn Fn(&S) -> Option<A>>,
        set: Box<dyn Fn(A, &S) -> S>,
    }
    
    impl<S: Clone + 'static, A: 'static> Affine<S, A> {
        pub fn over(&self, f: impl FnOnce(A) -> A, s: &S) -> S {
            match (self.preview)(s) {
                Some(v) => (self.set)(f(v), s),
                None    => s.clone(),
            }
        }
    }
    
    pub fn email_affine() -> Affine<User, String> {
        Affine::new(
            |u: &User| u.email.clone(),
            |e, u: &User| User { email: Some(e), ..u.clone() },
        )
    }
    

    Rust (zero-cost — trait dispatch)

    pub trait AffineTraversal {
        type Source: Clone;
        type Focus;
    
        fn preview(s: &Self::Source) -> Option<Self::Focus>;
        fn set(a: Self::Focus, s: &Self::Source) -> Self::Source;
    
        fn over(f: impl FnOnce(Self::Focus) -> Self::Focus, s: &Self::Source) -> Self::Source {
            match Self::preview(s) {
                Some(v) => Self::set(f(v), s),
                None    => s.clone(),
            }
        }
    }
    
    pub struct UserEmailAffine;
    
    impl AffineTraversal for UserEmailAffine {
        type Source = User;
        type Focus  = String;
    
        fn preview(u: &User) -> Option<String> { u.email.clone() }
        fn set(e: String, u: &User) -> User    { User { email: Some(e), ..u.clone() } }
    }
    

    Type Signatures

    ConceptOCamlRust (boxed)
    Affine type('s, 'a) affineAffine<S, A>
    Preview's -> 'a optionBox<dyn Fn(&S) -> Option<A>>
    Set'a -> 's -> 'sBox<dyn Fn(A, &S) -> S>
    Over('a -> 'a) -> 's -> 'sfn over(&self, f: impl FnOnce(A)->A, s:&S)->S
    Optional value'a optionOption<A>
    Record update syntax{ u with email = Some e }User { email: Some(e), ..u.clone() }

    Key Insights

  • Record update vs struct update: OCaml's { u with field = v } compiles to a structural copy; Rust's ..u.clone() is explicit — the .clone() call reminds the programmer that a heap allocation occurs. The clone is unavoidable here because the set closure must own its output.
  • Two dispatch strategies: Rust offers boxed closures (Box<dyn Fn(...)>) for runtime flexibility (the affine is a first-class value you can pass around) and trait objects for zero-cost compile-time dispatch. OCaml only has the record-of-closures style, which is closest to the boxed approach.
  • Lifetime tracking: OCaml's GC hides ownership. Rust's preview takes &S (borrow, no copy) while set takes an owned A and &S (borrow the whole), returning an owned S. The signature makes data flow explicit.
  • **'static bounds**: Because closures are stored inside the struct (Box<dyn Fn>), Rust requires 'static lifetime on S and A. In OCaml this constraint is invisible — the GC keeps everything alive.
  • HashMap affines: OCaml's Map.Make(String) functor produces a module; Rust uses HashMap<String, String> directly. Both express "focus on a key" with identical logic — a closure that captures the key and either reads or replaces the entry.
  • When to Use Each Style

    **Use boxed-closure Affine<S, A> when:** you need the affine to be a runtime value — stored in a Vec, chosen at runtime, or returned from a factory function. The heap allocation is a one-time cost at construction.

    **Use the AffineTraversal trait when:** the affine is known at compile time and you want zero overhead. The compiler monomorphises the over default method into a direct function call with no indirection.

    Exercises

  • Implement an affine traversal for Option<T> (same as a prism for Some but with set updating the inner value).
  • Write modify_at_key<K, V>(key: K, f: impl Fn(V) -> V, map: HashMap<K, V>) -> HashMap<K, V>.
  • Compose a lens (into a struct field containing a HashMap) with the at_key affine traversal.
  • Open Source Repos