Skip to main content

karpal_optics/
iso.rs

1use crate::fold::Fold;
2use crate::getter::Getter;
3use crate::optic::Optic;
4use crate::review::Review;
5use karpal_profunctor::Profunctor;
6
7/// An isomorphism witnesses that `S` and `A` are "the same" structure.
8///
9/// Only requires `Profunctor` — the weakest constraint of any optic.
10///
11/// `S` — source, `T` — modified source, `A` — focus, `B` — replacement.
12pub struct Iso<S, T, A, B> {
13    forward: fn(&S) -> A,
14    backward: fn(B) -> T,
15}
16
17/// A simple (monomorphic) iso where `S == T` and `A == B`.
18pub type SimpleIso<S, A> = Iso<S, S, A, A>;
19
20impl<S, T, A, B> Optic for Iso<S, T, A, B> {}
21
22impl<S, T, A, B> Iso<S, T, A, B> {
23    pub fn new(forward: fn(&S) -> A, backward: fn(B) -> T) -> Self {
24        Self { forward, backward }
25    }
26
27    /// Extract the focus from a source.
28    pub fn get(&self, s: &S) -> A {
29        (self.forward)(s)
30    }
31
32    /// Construct a source from a focus value (the reverse direction).
33    pub fn review(&self, b: B) -> T {
34        (self.backward)(b)
35    }
36
37    /// Set the focus, discarding the old value.
38    pub fn set(&self, _s: S, b: B) -> T {
39        (self.backward)(b)
40    }
41
42    /// Profunctor encoding: transform a `P<A, B>` into a `P<S, T>`.
43    ///
44    /// Only requires `Profunctor` — no `Strong` or `Choice` needed.
45    pub fn transform<P: Profunctor>(&self, pab: P::P<A, B>) -> P::P<S, T>
46    where
47        S: 'static,
48        T: 'static,
49        A: 'static,
50        B: 'static,
51    {
52        let fwd = self.forward;
53        let bwd = self.backward;
54        P::dimap(move |s: S| fwd(&s), bwd, pab)
55    }
56
57    /// Convert to a `Getter`.
58    pub fn to_getter(&self) -> Getter<S, A> {
59        Getter::new(self.forward)
60    }
61
62    /// Convert to a `Review`.
63    pub fn to_review(&self) -> Review<T, B> {
64        Review::new(self.backward)
65    }
66
67    /// Convert to a `Fold` (single-element).
68    pub fn to_fold(&self) -> Fold<S, A>
69    where
70        S: 'static,
71        A: 'static,
72    {
73        let fwd = self.forward;
74        Fold::new(move |s| vec![fwd(s)])
75    }
76}
77
78impl<S: Clone, T, A, B> Iso<S, T, A, B> {
79    /// Modify the focus.
80    pub fn over(&self, s: S, f: impl FnOnce(A) -> B) -> T {
81        (self.backward)(f((self.forward)(&s)))
82    }
83
84    /// Convert to a `ComposedLens` (uses boxed closures since iso's backward
85    /// doesn't match lens's `fn(S, B) -> T` signature).
86    pub fn to_lens(&self) -> crate::lens::ComposedLens<S, T, A, B>
87    where
88        S: 'static,
89        T: 'static,
90        A: 'static,
91        B: 'static,
92    {
93        let fwd = self.forward;
94        let bwd = self.backward;
95        crate::lens::ComposedLens::from_fns(Box::new(fwd), Box::new(move |_s, b| bwd(b)))
96    }
97
98    /// Convert to a `Setter`.
99    pub fn to_setter(&self) -> crate::setter::Setter<S, T, A, B>
100    where
101        S: 'static,
102        T: 'static,
103        A: 'static,
104        B: 'static,
105    {
106        let fwd = self.forward;
107        let bwd = self.backward;
108        crate::setter::Setter::new(move |s: S, f: &dyn Fn(A) -> B| bwd(f(fwd(&s))))
109    }
110
111    /// Convert to a `Traversal` (single-element).
112    pub fn to_traversal(&self) -> crate::traversal::Traversal<S, T, A, B>
113    where
114        S: 'static,
115        T: 'static,
116        A: 'static,
117        B: 'static,
118    {
119        let fwd = self.forward;
120        let bwd = self.backward;
121        crate::traversal::Traversal::new(move |s| vec![fwd(s)], move |s, f| bwd(f(fwd(&s))))
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use karpal_profunctor::FnP;
129    use proptest::prelude::*;
130
131    fn celsius_fahrenheit_iso() -> SimpleIso<f64, f64> {
132        Iso::new(
133            |c: &f64| c * 9.0 / 5.0 + 32.0,
134            |f: f64| (f - 32.0) * 5.0 / 9.0,
135        )
136    }
137
138    fn string_bytes_iso() -> SimpleIso<String, Vec<u8>> {
139        Iso::new(
140            |s: &String| s.clone().into_bytes(),
141            |b: Vec<u8>| String::from_utf8(b).unwrap(),
142        )
143    }
144
145    #[test]
146    fn iso_get() {
147        let iso = celsius_fahrenheit_iso();
148        let result = iso.get(&100.0);
149        assert!((result - 212.0).abs() < 1e-10);
150    }
151
152    #[test]
153    fn iso_review() {
154        let iso = celsius_fahrenheit_iso();
155        let result = iso.review(212.0);
156        assert!((result - 100.0).abs() < 1e-10);
157    }
158
159    #[test]
160    fn iso_over() {
161        let iso = celsius_fahrenheit_iso();
162        // Convert 0°C to F, add 10 to F, convert back
163        let result = iso.over(0.0, |f| f + 18.0);
164        assert!((result - 10.0).abs() < 1e-10);
165    }
166
167    #[test]
168    fn iso_set() {
169        let iso = celsius_fahrenheit_iso();
170        let result = iso.set(999.0, 32.0); // set F to 32 → 0°C
171        assert!((result - 0.0).abs() < 1e-10);
172    }
173
174    #[test]
175    fn iso_transform_fnp() {
176        let iso = string_bytes_iso();
177        let upper: Box<dyn Fn(Vec<u8>) -> Vec<u8>> =
178            Box::new(|bytes| bytes.into_iter().map(|b| b.to_ascii_uppercase()).collect());
179        let f = iso.transform::<FnP>(upper);
180        assert_eq!(f("hello".to_string()), "HELLO");
181    }
182
183    #[test]
184    fn iso_to_lens() {
185        let iso = string_bytes_iso();
186        let lens = iso.to_lens();
187        assert_eq!(lens.get(&"hi".to_string()), vec![b'h', b'i']);
188        assert_eq!(lens.set("x".to_string(), vec![b'a', b'b']), "ab");
189    }
190
191    #[test]
192    fn iso_to_getter() {
193        let iso = string_bytes_iso();
194        let getter = iso.to_getter();
195        assert_eq!(getter.get(&"hi".to_string()), vec![b'h', b'i']);
196    }
197
198    #[test]
199    fn iso_to_review() {
200        let iso = string_bytes_iso();
201        let review = iso.to_review();
202        assert_eq!(review.review(vec![b'a', b'b']), "ab");
203    }
204
205    // Roundtrip law: backward(forward(s)) == s
206    proptest! {
207        #[test]
208        fn law_roundtrip_forward_backward(bytes in prop::collection::vec(any::<u8>(), 0..20)) {
209            let iso = string_bytes_iso();
210            // Only test valid UTF-8 sequences
211            if let Ok(s) = String::from_utf8(bytes) {
212                let result = iso.review(iso.get(&s));
213                prop_assert_eq!(result, s);
214            }
215        }
216    }
217
218    // Roundtrip law: forward(backward(b)) == b
219    proptest! {
220        #[test]
221        fn law_roundtrip_backward_forward(s in "[a-z]{0,20}") {
222            let iso = string_bytes_iso();
223            let result = iso.get(&iso.review(iso.get(&s)));
224            prop_assert_eq!(result, iso.get(&s));
225        }
226    }
227}