Skip to main content

karpal_optics/
lens.rs

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