// Example 209: Affine Traversal โ At Most One Focus
use std::collections::HashMap;
struct Affine<S, A> {
preview: Box<dyn Fn(&S) -> Option<A>>,
set: Box<dyn Fn(A, &S) -> S>,
}
impl<S: 'static, A: 'static> Affine<S, A> {
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) }
}
fn over(&self, f: impl FnOnce(A) -> A, s: &S) -> S where S: Clone {
match (self.preview)(s) {
Some(v) => (self.set)(f(v), s),
None => s.clone(),
}
}
}
// Approach 1: Affine for optional fields
#[derive(Debug, Clone, PartialEq)]
struct User {
name: String,
email: Option<String>,
phone: Option<String>,
}
fn email_affine() -> Affine<User, String> {
Affine::new(
|u| u.email.clone(),
|e, u| User { email: Some(e), ..u.clone() },
)
}
fn phone_affine() -> Affine<User, String> {
Affine::new(
|u| u.phone.clone(),
|p, u| User { phone: Some(p), ..u.clone() },
)
}
// Approach 2: Affine for HashMap lookups
fn at_key(key: String) -> Affine<HashMap<String, String>, String> {
let k1 = key.clone();
let k2 = key;
Affine::new(
move |m| m.get(&k1).cloned(),
move |v, m| { let mut m2 = m.clone(); m2.insert(k2.clone(), v); m2 },
)
}
// Approach 3: Composition
fn compose_affine<S: Clone + 'static, A: Clone + 'static, B: 'static>(
outer: Affine<S, A>,
inner: Affine<A, B>,
) -> Affine<S, B> {
let op = outer.preview;
let os = outer.set;
let ip = inner.preview;
let is = inner.set;
Affine::new(
move |s| (op)(s).and_then(|a| (ip)(&a)),
move |b, s| match (op)(s) {
Some(a) => (os)((is)(b, &a), s),
None => s.clone(),
},
)
}
fn main() {
let user1 = User { name: "Alice".into(), email: Some("alice@x.com".into()), phone: None };
let user2 = User { name: "Bob".into(), email: None, phone: Some("555-1234".into()) };
let ea = email_affine();
assert_eq!((ea.preview)(&user1), Some("alice@x.com".into()));
assert_eq!((ea.preview)(&user2), None);
// Set
let u = (ea.set)("new@x.com".into(), &user1);
assert_eq!(u.email, Some("new@x.com".into()));
// Over on present
let u2 = email_affine().over(|e| e.to_uppercase(), &user1);
assert_eq!(u2.email, Some("ALICE@X.COM".into()));
// Over on absent = no-op
let u3 = email_affine().over(|e| e.to_uppercase(), &user2);
assert_eq!(u3.email, None);
// HashMap lookup
let m: HashMap<String, String> = [("a".into(), "1".into()), ("b".into(), "2".into())].into();
let at_a = at_key("a".into());
assert_eq!((at_a.preview)(&m), Some("1".into()));
assert_eq!((at_key("z".into()).preview)(&m), None);
println!("โ All tests passed");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_affine_preview() {
let u = User { name: "X".into(), email: Some("x@y".into()), phone: None };
assert_eq!((email_affine().preview)(&u), Some("x@y".into()));
assert_eq!((phone_affine().preview)(&u), None);
}
#[test]
fn test_affine_over_absent() {
let u = User { name: "X".into(), email: None, phone: None };
let u2 = email_affine().over(|_| "forced".into(), &u);
assert_eq!(u2.email, None); // no-op
}
#[test]
fn test_hashmap_affine() {
let m: HashMap<String, String> = [("k".into(), "v".into())].into();
let a = at_key("k".into());
assert_eq!((a.preview)(&m), Some("v".into()));
let m2 = (a.set)("new".into(), &m);
assert_eq!(m2.get("k"), Some(&"new".into()));
}
}
(* Example 209: Affine Traversal โ At Most One Focus *)
(* An affine traversal focuses on at most one value.
It's the combination of a prism (might not exist) and a lens (if it exists, it's unique).
Think: "optional field accessor" *)
type ('s, 'a) affine = {
preview : 's -> 'a option;
set : 'a -> 's -> 's;
}
(* Approach 1: Affine for optional record fields *)
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 });
}
let phone_affine : (user, string) affine = {
preview = (fun u -> u.phone);
set = (fun p u -> { u with phone = Some p });
}
(* Approach 2: Affine for map lookups *)
module StringMap = Map.Make(String)
let at_key (key : string) : (string StringMap.t, string) affine = {
preview = (fun m -> StringMap.find_opt key m);
set = (fun v m -> StringMap.add key v m);
}
(* Approach 3: Affine combinators *)
let over_affine (a : ('s, 'a) affine) (f : 'a -> 'a) (s : 's) : 's =
match a.preview s with
| Some v -> a.set (f v) s
| None -> s
let compose_affine (outer : ('s, 'a) affine) (inner : ('a, 'b) affine) : ('s, 'b) affine = {
preview = (fun s ->
match outer.preview s with
| Some a -> inner.preview a
| None -> None);
set = (fun b s ->
match outer.preview s with
| Some a -> outer.set (inner.set b a) s
| None -> s);
}
(* === Tests === *)
let () =
let user1 = { name = "Alice"; email = Some "alice@x.com"; phone = None } in
let user2 = { name = "Bob"; email = None; phone = Some "555-1234" } in
(* Preview *)
assert (email_affine.preview user1 = Some "alice@x.com");
assert (email_affine.preview user2 = None);
assert (phone_affine.preview user2 = Some "555-1234");
(* Set *)
let u = email_affine.set "new@x.com" user1 in
assert (u.email = Some "new@x.com");
(* Over *)
let u2 = over_affine email_affine String.uppercase_ascii user1 in
assert (u2.email = Some "ALICE@X.COM");
(* Over on missing field is no-op *)
let u3 = over_affine email_affine String.uppercase_ascii user2 in
assert (u3.email = None);
(* Map lookup *)
let m = StringMap.of_seq (List.to_seq [("a", "1"); ("b", "2")]) in
let at_a = at_key "a" in
assert (at_a.preview m = Some "1");
assert ((at_key "z").preview m = None);
let m2 = at_a.set "99" m in
assert (at_a.preview m2 = Some "99");
print_endline "โ All tests passed"