Skip to main content

karpal_optics/
iso.rs

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