Skip to main content

karpal_core/
invariant.rs

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