Skip to main content

karpal_core/
invariant.rs

1use crate::hkt::{EnvF, HKT, IdentityF, OptionF, ResultF};
2#[cfg(any(feature = "std", feature = "alloc"))]
3use crate::hkt::{NonEmptyVec, NonEmptyVecF, VecF};
4
5/// Invariant functor: maps with both a covariant and contravariant function.
6///
7/// Every covariant Functor is trivially Invariant (ignoring `g`).
8/// Every Contravariant is also Invariant (ignoring `f`).
9///
10/// Laws:
11/// - Identity: `invmap(fa, id, id) == fa`
12/// - Composition: `invmap(fa, g1 . f1, f2 . g2) == invmap(invmap(fa, f1, f2), g1, g2)`
13pub trait Invariant: HKT {
14    fn invmap<A, B>(fa: Self::Of<A>, f: impl Fn(A) -> B, g: impl Fn(B) -> A) -> Self::Of<B>;
15}
16
17impl Invariant for OptionF {
18    fn invmap<A, B>(fa: Option<A>, f: impl Fn(A) -> B, _g: impl Fn(B) -> A) -> Option<B> {
19        fa.map(f)
20    }
21}
22
23impl<E> Invariant for ResultF<E> {
24    fn invmap<A, B>(fa: Result<A, E>, f: impl Fn(A) -> B, _g: impl Fn(B) -> A) -> Result<B, E> {
25        fa.map(f)
26    }
27}
28
29#[cfg(any(feature = "std", feature = "alloc"))]
30impl Invariant for VecF {
31    fn invmap<A, B>(fa: Vec<A>, f: impl Fn(A) -> B, _g: impl Fn(B) -> A) -> Vec<B> {
32        fa.into_iter().map(f).collect()
33    }
34}
35
36impl Invariant for IdentityF {
37    fn invmap<A, B>(fa: A, f: impl Fn(A) -> B, _g: impl Fn(B) -> A) -> B {
38        f(fa)
39    }
40}
41
42#[cfg(any(feature = "std", feature = "alloc"))]
43impl Invariant for NonEmptyVecF {
44    fn invmap<A, B>(fa: NonEmptyVec<A>, f: impl Fn(A) -> B, _g: impl Fn(B) -> A) -> NonEmptyVec<B> {
45        NonEmptyVec::new(f(fa.head), fa.tail.into_iter().map(&f).collect())
46    }
47}
48
49impl<E> Invariant for EnvF<E> {
50    fn invmap<A, B>(fa: (E, A), f: impl Fn(A) -> B, _g: impl Fn(B) -> A) -> (E, B) {
51        (fa.0, f(fa.1))
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn option_invmap() {
61        let result = OptionF::invmap(Some(3), |x| x * 2, |x| x / 2);
62        assert_eq!(result, Some(6));
63    }
64
65    #[test]
66    fn option_invmap_none() {
67        let result = OptionF::invmap(None::<i32>, |x| x * 2, |x| x / 2);
68        assert_eq!(result, None);
69    }
70
71    #[test]
72    fn result_invmap() {
73        let result = ResultF::<&str>::invmap(Ok(5), |x| x + 1, |x| x - 1);
74        assert_eq!(result, Ok(6));
75    }
76
77    #[test]
78    fn vec_invmap() {
79        let result = VecF::invmap(vec![1, 2, 3], |x| x * 2, |x| x / 2);
80        assert_eq!(result, vec![2, 4, 6]);
81    }
82
83    #[test]
84    fn identity_invmap() {
85        let result = IdentityF::invmap(42, |x| x + 1, |x| x - 1);
86        assert_eq!(result, 43);
87    }
88
89    #[test]
90    fn nonemptyvec_invmap() {
91        let nev = NonEmptyVec::new(1, vec![2, 3]);
92        let result = NonEmptyVecF::invmap(nev, |x| x * 10, |x| x / 10);
93        assert_eq!(result, NonEmptyVec::new(10, vec![20, 30]));
94    }
95
96    #[test]
97    fn env_invmap() {
98        let result = EnvF::<&str>::invmap(("hello", 42), |x| x + 1, |x| x - 1);
99        assert_eq!(result, ("hello", 43));
100    }
101}
102
103#[cfg(test)]
104mod law_tests {
105    use super::*;
106    use proptest::prelude::*;
107
108    proptest! {
109        // Identity: invmap(fa, id, id) == fa
110        #[test]
111        fn option_identity(x in any::<Option<i32>>()) {
112            let result = OptionF::invmap(x, |a| a, |a| a);
113            prop_assert_eq!(result, x);
114        }
115
116        // Composition: invmap(fa, g1 . f1, f2 . g2) == invmap(invmap(fa, f1, f2), g1, g2)
117        #[test]
118        fn option_composition(x in any::<Option<i16>>()) {
119            let f1 = |a: i16| a.wrapping_add(1);
120            let f2 = |a: i16| a.wrapping_sub(1);
121            let g1 = |a: i16| a.wrapping_mul(2);
122            let g2 = |a: i16| a / 2; // approximate inverse
123
124            let left = OptionF::invmap(x, |a| g1(f1(a)), |a| f2(g2(a)));
125            let right = OptionF::invmap(OptionF::invmap(x, f1, f2), g1, g2);
126            prop_assert_eq!(left, right);
127        }
128
129        #[test]
130        fn vec_identity(x in prop::collection::vec(any::<i32>(), 0..10)) {
131            let result = VecF::invmap(x.clone(), |a| a, |a| a);
132            prop_assert_eq!(result, x);
133        }
134
135        #[test]
136        fn vec_composition(x in prop::collection::vec(any::<i16>(), 0..10)) {
137            let f1 = |a: i16| a.wrapping_add(1);
138            let f2 = |a: i16| a.wrapping_sub(1);
139            let g1 = |a: i16| a.wrapping_mul(2);
140            let g2 = |a: i16| a / 2;
141
142            let left = VecF::invmap(x.clone(), |a| g1(f1(a)), |a| f2(g2(a)));
143            let right = VecF::invmap(VecF::invmap(x, f1, f2), g1, g2);
144            prop_assert_eq!(left, right);
145        }
146    }
147}