Skip to main content

karpal_optics/
lens.rs

1use std::rc::Rc;
2
3use crate::fold::Fold;
4use crate::getter::Getter;
5use crate::optic::Optic;
6use crate::setter::Setter;
7use crate::traversal::Traversal;
8use karpal_profunctor::strong::Strong;
9
10/// A composed lens built from two lenses chained together.
11///
12/// Unlike [`Lens`], which stores `fn` pointers, a composed lens stores
13/// boxed closures because closure composition cannot produce `fn` pointers.
14///
15/// For profunctor-level composition, use nested [`Lens::transform`] calls
16/// instead: `outer.transform::<P>(inner.transform::<P>(pab))`. This avoids
17/// the need for `Rc`/`Arc` to share closures.
18pub struct ComposedLens<S, T, X, Y> {
19    getter: Box<dyn Fn(&S) -> X>,
20    setter: Box<dyn Fn(S, Y) -> T>,
21}
22
23/// A simple (monomorphic) composed lens where `S == T` and `X == Y`.
24pub type SimpleComposedLens<S, X> = ComposedLens<S, S, X, X>;
25
26impl<S, T, X, Y> Optic for ComposedLens<S, T, X, Y> {}
27
28impl<S, T, X, Y> ComposedLens<S, T, X, Y> {
29    /// Create a composed lens directly from boxed closures.
30    pub fn from_fns(getter: Box<dyn Fn(&S) -> X>, setter: Box<dyn Fn(S, Y) -> T>) -> Self {
31        Self { getter, setter }
32    }
33
34    pub fn get(&self, s: &S) -> X {
35        (self.getter)(s)
36    }
37
38    pub fn set(&self, s: S, y: Y) -> T {
39        (self.setter)(s, y)
40    }
41}
42
43impl<S: Clone, T, X, Y> ComposedLens<S, T, X, Y> {
44    pub fn over(&self, s: S, f: impl FnOnce(X) -> Y) -> T {
45        let x = (self.getter)(&s);
46        (self.setter)(s, f(x))
47    }
48
49    /// Chain another lens to focus deeper.
50    pub fn then<U, V>(self, inner: Lens<X, Y, U, V>) -> ComposedLens<S, T, U, V>
51    where
52        S: 'static,
53        T: 'static,
54        X: 'static,
55        Y: 'static,
56        U: 'static,
57        V: 'static,
58    {
59        let outer_getter: Rc<dyn Fn(&S) -> X> = self.getter.into();
60        let outer_setter = self.setter;
61        let inner_getter = inner.getter;
62        let inner_setter = inner.setter;
63        let og = Rc::clone(&outer_getter);
64        ComposedLens {
65            getter: Box::new(move |s: &S| inner_getter(&outer_getter(s))),
66            setter: Box::new(move |s: S, v: V| {
67                let x = og(&s);
68                let y = inner_setter(x, v);
69                (outer_setter)(s, y)
70            }),
71        }
72    }
73}
74
75/// A van Laarhoven–style lens encoded with getter/setter function pointers.
76///
77/// `S` — source type, `T` — modified source type,
78/// `A` — focus type, `B` — replacement type.
79///
80/// For simple (non-polymorphic) lenses, use [`SimpleLens`].
81pub struct Lens<S, T, A, B> {
82    getter: fn(&S) -> A,
83    setter: fn(S, B) -> T,
84}
85
86/// A simple (monomorphic) lens where `S == T` and `A == B`.
87pub type SimpleLens<S, A> = Lens<S, S, A, A>;
88
89impl<S, T, A, B> Optic for Lens<S, T, A, B> {}
90
91impl<S, T, A, B> Lens<S, T, A, B> {
92    pub fn new(getter: fn(&S) -> A, setter: fn(S, B) -> T) -> Self {
93        Self { getter, setter }
94    }
95
96    pub fn get(&self, s: &S) -> A {
97        (self.getter)(s)
98    }
99
100    pub fn set(&self, s: S, b: B) -> T {
101        (self.setter)(s, b)
102    }
103
104    /// Chain another lens to focus deeper, producing a [`ComposedLens`].
105    pub fn then<X, Y>(self, inner: Lens<A, B, X, Y>) -> ComposedLens<S, T, X, Y>
106    where
107        S: 'static,
108        T: 'static,
109        A: 'static,
110        B: 'static,
111        X: 'static,
112        Y: 'static,
113    {
114        let outer_getter = self.getter;
115        let outer_setter = self.setter;
116        let inner_getter = inner.getter;
117        let inner_setter = inner.setter;
118        ComposedLens {
119            getter: Box::new(move |s: &S| inner_getter(&outer_getter(s))),
120            setter: Box::new(move |s: S, y: Y| {
121                let a = outer_getter(&s);
122                let b = inner_setter(a, y);
123                outer_setter(s, b)
124            }),
125        }
126    }
127}
128
129impl<S, T, A, B> Lens<S, T, A, B> {
130    /// Convert to a `Getter` (read-only, discards setter).
131    pub fn to_getter(&self) -> Getter<S, A> {
132        Getter::new(self.getter)
133    }
134
135    /// Convert to a `Setter` (modify-only).
136    pub fn to_setter(&self) -> Setter<S, T, A, B>
137    where
138        S: 'static,
139        T: 'static,
140        A: 'static,
141        B: 'static,
142    {
143        let getter = self.getter;
144        let setter = self.setter;
145        Setter::new(move |s: S, f: &dyn Fn(A) -> B| {
146            let a = getter(&s);
147            setter(s, f(a))
148        })
149    }
150
151    /// Convert to a `Traversal` (single-element focus).
152    pub fn to_traversal(&self) -> Traversal<S, T, A, B>
153    where
154        S: 'static,
155        T: 'static,
156        A: 'static,
157        B: 'static,
158    {
159        let getter = self.getter;
160        let setter = self.setter;
161        Traversal::new(
162            move |s| vec![getter(s)],
163            move |s, f| {
164                let a = getter(&s);
165                setter(s, f(a))
166            },
167        )
168    }
169
170    /// Convert to a `Fold` (single-element, read-only).
171    pub fn to_fold(&self) -> Fold<S, A>
172    where
173        S: 'static,
174        A: 'static,
175    {
176        let getter = self.getter;
177        Fold::new(move |s| vec![getter(s)])
178    }
179}
180
181impl<S: Clone, T, A, B> Lens<S, T, A, B> {
182    pub fn over(&self, s: S, f: impl FnOnce(A) -> B) -> T {
183        let a = (self.getter)(&s);
184        (self.setter)(s, f(a))
185    }
186
187    /// Profunctor encoding: transform a `P<A, B>` into a `P<S, T>` via this lens.
188    ///
189    /// This is the key operation that connects concrete lenses to the profunctor
190    /// hierarchy. Given any `Strong` profunctor `P` and a value `pab: P<A, B>`,
191    /// `transform` produces `P<S, T>` by:
192    ///
193    /// 1. `first(pab)` lifts to `P<(A, S), (B, S)>`
194    /// 2. `dimap` pre-composes with `s -> (get(s), s)` and post-composes with `(b, s) -> set(s, b)`
195    pub fn transform<P: Strong>(&self, pab: P::P<A, B>) -> P::P<S, T>
196    where
197        S: 'static,
198        T: 'static,
199        A: 'static,
200        B: 'static,
201    {
202        let getter = self.getter;
203        let setter = self.setter;
204        let first_pab = P::first::<A, B, S>(pab);
205        P::dimap(
206            move |s: S| {
207                let a = getter(&s);
208                (a, s)
209            },
210            move |(b, s)| setter(s, b),
211            first_pab,
212        )
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use karpal_profunctor::FnP;
220    use proptest::prelude::*;
221
222    #[derive(Debug, Clone, PartialEq)]
223    struct Person {
224        name: String,
225        age: u32,
226    }
227
228    fn person_name_lens() -> SimpleLens<Person, String> {
229        Lens::new(|p: &Person| p.name.clone(), |p, name| Person { name, ..p })
230    }
231
232    fn person_age_lens() -> SimpleLens<Person, u32> {
233        Lens::new(|p: &Person| p.age, |p, age| Person { age, ..p })
234    }
235
236    fn sample_person() -> Person {
237        Person {
238            name: "Alice".to_string(),
239            age: 30,
240        }
241    }
242
243    #[test]
244    fn lens_get() {
245        let lens = person_name_lens();
246        assert_eq!(lens.get(&sample_person()), "Alice");
247    }
248
249    #[test]
250    fn lens_set() {
251        let lens = person_name_lens();
252        let updated = lens.set(sample_person(), "Bob".to_string());
253        assert_eq!(updated.name, "Bob");
254        assert_eq!(updated.age, 30);
255    }
256
257    #[test]
258    fn lens_over() {
259        let lens = person_age_lens();
260        let updated = lens.over(sample_person(), |age| age + 1);
261        assert_eq!(updated.age, 31);
262        assert_eq!(updated.name, "Alice");
263    }
264
265    // Lens laws
266    // GetPut: set(s, get(s)) == s
267    #[test]
268    fn law_get_put() {
269        let lens = person_name_lens();
270        let p = sample_person();
271        let result = lens.set(p.clone(), lens.get(&p));
272        assert_eq!(result, p);
273    }
274
275    // PutGet: get(set(s, b)) == b
276    #[test]
277    fn law_put_get() {
278        let lens = person_name_lens();
279        let result = lens.set(sample_person(), "Bob".to_string());
280        assert_eq!(lens.get(&result), "Bob");
281    }
282
283    // PutPut: set(set(s, b1), b2) == set(s, b2)
284    #[test]
285    fn law_put_put() {
286        let lens = person_name_lens();
287        let p = sample_person();
288        let left = lens.set(
289            lens.set(p.clone(), "Bob".to_string()),
290            "Charlie".to_string(),
291        );
292        let right = lens.set(p, "Charlie".to_string());
293        assert_eq!(left, right);
294    }
295
296    // Integration test: run lens through FnP profunctor
297    #[test]
298    fn lens_transform_fnp() {
299        let lens = person_age_lens();
300        let increment: Box<dyn Fn(u32) -> u32> = Box::new(|age| age + 1);
301        let transform_fn = lens.transform::<FnP>(increment);
302        let result = transform_fn(sample_person());
303        assert_eq!(result.age, 31);
304        assert_eq!(result.name, "Alice");
305    }
306
307    #[test]
308    fn lens_transform_fnp_name() {
309        let lens = person_name_lens();
310        let upper: Box<dyn Fn(String) -> String> = Box::new(|s| s.to_uppercase());
311        let transform_fn = lens.transform::<FnP>(upper);
312        let result = transform_fn(sample_person());
313        assert_eq!(result.name, "ALICE");
314        assert_eq!(result.age, 30);
315    }
316
317    // --- Composition tests ---
318
319    #[derive(Debug, Clone, PartialEq)]
320    struct Address {
321        street: String,
322        city: String,
323    }
324
325    #[derive(Debug, Clone, PartialEq)]
326    struct Company {
327        name: String,
328        ceo: Person,
329    }
330
331    fn company_ceo_lens() -> SimpleLens<Company, Person> {
332        Lens::new(|c: &Company| c.ceo.clone(), |c, ceo| Company { ceo, ..c })
333    }
334
335    fn address_city_lens() -> SimpleLens<Address, String> {
336        Lens::new(
337            |a: &Address| a.city.clone(),
338            |a, city| Address { city, ..a },
339        )
340    }
341
342    fn address_street_lens() -> SimpleLens<Address, String> {
343        Lens::new(
344            |a: &Address| a.street.clone(),
345            |a, street| Address { street, ..a },
346        )
347    }
348
349    fn sample_company() -> Company {
350        Company {
351            name: "Acme".to_string(),
352            ceo: sample_person(),
353        }
354    }
355
356    // Two-deep composition: Company → ceo → name
357    #[test]
358    fn composed_get() {
359        let lens = company_ceo_lens().then(person_name_lens());
360        assert_eq!(lens.get(&sample_company()), "Alice");
361    }
362
363    #[test]
364    fn composed_set() {
365        let lens = company_ceo_lens().then(person_name_lens());
366        let updated = lens.set(sample_company(), "Bob".to_string());
367        assert_eq!(updated.ceo.name, "Bob");
368        assert_eq!(updated.ceo.age, 30);
369        assert_eq!(updated.name, "Acme");
370    }
371
372    #[test]
373    fn composed_over() {
374        let lens = company_ceo_lens().then(person_age_lens());
375        let updated = lens.over(sample_company(), |age| age + 1);
376        assert_eq!(updated.ceo.age, 31);
377        assert_eq!(updated.ceo.name, "Alice");
378    }
379
380    // Three-deep: use a PersonWithAddr for a longer chain.
381
382    #[derive(Debug, Clone, PartialEq)]
383    struct PersonWithAddr {
384        name: String,
385        addr: Address,
386    }
387
388    #[derive(Debug, Clone, PartialEq)]
389    struct Org {
390        title: String,
391        lead: PersonWithAddr,
392    }
393
394    fn org_lead_lens() -> SimpleLens<Org, PersonWithAddr> {
395        Lens::new(|o: &Org| o.lead.clone(), |o, lead| Org { lead, ..o })
396    }
397
398    fn pwa_addr_lens() -> SimpleLens<PersonWithAddr, Address> {
399        Lens::new(
400            |p: &PersonWithAddr| p.addr.clone(),
401            |p, addr| PersonWithAddr { addr, ..p },
402        )
403    }
404
405    fn sample_org() -> Org {
406        Org {
407            title: "R&D".to_string(),
408            lead: PersonWithAddr {
409                name: "Alice".to_string(),
410                addr: Address {
411                    street: "123 Main St".to_string(),
412                    city: "Springfield".to_string(),
413                },
414            },
415        }
416    }
417
418    #[test]
419    fn three_deep_get() {
420        let lens = org_lead_lens()
421            .then(pwa_addr_lens())
422            .then(address_city_lens());
423        assert_eq!(lens.get(&sample_org()), "Springfield");
424    }
425
426    #[test]
427    fn three_deep_set() {
428        let lens = org_lead_lens()
429            .then(pwa_addr_lens())
430            .then(address_city_lens());
431        let updated = lens.set(sample_org(), "Shelbyville".to_string());
432        assert_eq!(updated.lead.addr.city, "Shelbyville");
433        assert_eq!(updated.lead.addr.street, "123 Main St");
434        assert_eq!(updated.lead.name, "Alice");
435    }
436
437    #[test]
438    fn three_deep_over() {
439        let lens = org_lead_lens()
440            .then(pwa_addr_lens())
441            .then(address_street_lens());
442        let updated = lens.over(sample_org(), |s| s.to_uppercase());
443        assert_eq!(updated.lead.addr.street, "123 MAIN ST");
444    }
445
446    // Composed lens law tests (proptest)
447    // Testing company_ceo().then(person_age()) since age is easy to generate.
448
449    proptest! {
450        // GetPut: set(s, get(s)) == s
451        #[test]
452        fn composed_law_get_put(name in "[a-z]{1,8}", co_name in "[a-z]{1,8}", age in 0u32..1000) {
453            let lens = company_ceo_lens().then(person_age_lens());
454            let c = Company {
455                name: co_name,
456                ceo: Person { name, age },
457            };
458            let result = lens.set(c.clone(), lens.get(&c));
459            prop_assert_eq!(result, c);
460        }
461
462        // PutGet: get(set(s, b)) == b
463        #[test]
464        fn composed_law_put_get(name in "[a-z]{1,8}", co_name in "[a-z]{1,8}", age in 0u32..1000, new_age in 0u32..1000) {
465            let lens = company_ceo_lens().then(person_age_lens());
466            let c = Company {
467                name: co_name,
468                ceo: Person { name, age },
469            };
470            let result = lens.set(c, new_age);
471            prop_assert_eq!(lens.get(&result), new_age);
472        }
473
474        // PutPut: set(set(s, b1), b2) == set(s, b2)
475        #[test]
476        fn composed_law_put_put(name in "[a-z]{1,8}", co_name in "[a-z]{1,8}", age in 0u32..1000, b1 in 0u32..1000, b2 in 0u32..1000) {
477            let lens = company_ceo_lens().then(person_age_lens());
478            let c = Company {
479                name: co_name,
480                ceo: Person { name, age },
481            };
482            let left = lens.set(lens.set(c.clone(), b1), b2);
483            let right = lens.set(c, b2);
484            prop_assert_eq!(left, right);
485        }
486    }
487
488    // Profunctor equivalence: outer.transform(inner.transform(f)) matches composed.over
489    #[test]
490    fn profunctor_composition_equivalence() {
491        let outer = company_ceo_lens();
492        let inner = person_age_lens();
493        let composed = company_ceo_lens().then(person_age_lens());
494
495        let increment: Box<dyn Fn(u32) -> u32> = Box::new(|age| age + 1);
496        let transform_fn = outer.transform::<FnP>(inner.transform::<FnP>(increment));
497
498        let c = sample_company();
499        let via_profunctor = transform_fn(c.clone());
500        let via_composed = composed.over(c, |age| age + 1);
501        assert_eq!(via_profunctor, via_composed);
502    }
503}