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