Skip to main content

karpal_optics/
lens.rs

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