Skip to main content

karpal_free/
day.rs

1#[cfg(feature = "std")]
2use std::boxed::Box;
3
4#[cfg(all(not(feature = "std"), feature = "alloc"))]
5use alloc::boxed::Box;
6
7#[cfg(feature = "std")]
8use std::rc::Rc;
9
10#[cfg(all(not(feature = "std"), feature = "alloc"))]
11use alloc::rc::Rc;
12
13use core::marker::PhantomData;
14
15use karpal_core::applicative::Applicative;
16use karpal_core::hkt::HKT;
17use karpal_core::natural::NaturalTransformation;
18
19/// Day Convolution — combines two functors `F` and `G` into a single
20/// computation.
21///
22/// `Day<F, G, A, B, C> ≅ (F B, G C, B → C → A)`
23///
24/// Stores an `F` value, a `G` value, and a combining function.
25/// The intermediate types `B` and `C` are visible in the type signature
26/// (Rust lacks first-class existential types, same as `Lan` and `Coyoneda`).
27///
28/// # Key properties
29///
30/// - **Functor in A**: `fmap` composes onto the combining function,
31///   no bounds on F or G needed.
32/// - **Interpretation**: `run_day` uses two natural transformations
33///   (one for F, one for G) to interpret into a target Applicative.
34pub struct Day<F: HKT, G: HKT, A, B, C> {
35    f_val: F::Of<B>,
36    g_val: G::Of<C>,
37    combine: Box<dyn Fn(B, C) -> A>,
38    _marker: PhantomData<(F, G)>,
39}
40
41impl<F: HKT + 'static, G: HKT + 'static, A: 'static, B: Clone + 'static, C: Clone + 'static>
42    Day<F, G, A, B, C>
43{
44    /// Construct a Day from two functor values and a combining function.
45    pub fn new(f_val: F::Of<B>, g_val: G::Of<C>, combine: impl Fn(B, C) -> A + 'static) -> Self {
46        Day {
47            f_val,
48            g_val,
49            combine: Box::new(combine),
50            _marker: PhantomData,
51        }
52    }
53
54    /// Map a function over the result. No bounds on F or G required.
55    pub fn fmap<D: 'static>(self, f: impl Fn(A) -> D + 'static) -> Day<F, G, D, B, C> {
56        let old_combine = self.combine;
57        Day {
58            f_val: self.f_val,
59            g_val: self.g_val,
60            combine: Box::new(move |b, c| f(old_combine(b, c))),
61            _marker: PhantomData,
62        }
63    }
64
65    /// Interpret this Day value into a target applicative `M` using
66    /// two natural transformations: `NF: F ~> M` and `NG: G ~> M`.
67    pub fn run_day<M, NF, NG>(self) -> M::Of<A>
68    where
69        M: Applicative,
70        NF: NaturalTransformation<F, M>,
71        NG: NaturalTransformation<G, M>,
72    {
73        let m_b: M::Of<B> = NF::transform(self.f_val);
74        let m_c: M::Of<C> = NG::transform(self.g_val);
75        let combine = Rc::new(self.combine);
76        let m_curried: M::Of<Box<dyn Fn(C) -> A>> =
77            M::fmap(m_b, move |b: B| -> Box<dyn Fn(C) -> A> {
78                let combine = combine.clone();
79                Box::new(move |c: C| combine(b.clone(), c))
80            });
81        M::ap(m_curried, m_c)
82    }
83}
84
85/// Marker type for `Day<F, G, _, B, C>`.
86///
87/// Note: Cannot implement `HKT` or `Functor` due to Rust's GAT limitations
88/// (extra type parameters F, G, B, C cannot be threaded through `type Of<T>`).
89/// Use `Day::fmap` directly.
90pub struct DayF<F: HKT + 'static, G: HKT + 'static>(PhantomData<(F, G)>);
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use karpal_core::hkt::OptionF;
96
97    struct OptionId;
98    impl NaturalTransformation<OptionF, OptionF> for OptionId {
99        fn transform<A>(fa: Option<A>) -> Option<A> {
100            fa
101        }
102    }
103
104    #[test]
105    fn new_and_run_day() {
106        let day = Day::<OptionF, OptionF, i32, i32, i32>::new(Some(3), Some(4), |a, b| a * b);
107        let result = day.run_day::<OptionF, OptionId, OptionId>();
108        assert_eq!(result, Some(12));
109    }
110
111    #[test]
112    fn run_day_none_left() {
113        let day = Day::<OptionF, OptionF, i32, i32, i32>::new(None::<i32>, Some(4), |a, b| a + b);
114        let result = day.run_day::<OptionF, OptionId, OptionId>();
115        assert_eq!(result, None);
116    }
117
118    #[test]
119    fn run_day_none_right() {
120        let day = Day::<OptionF, OptionF, i32, i32, i32>::new(Some(3), None::<i32>, |a, b| a + b);
121        let result = day.run_day::<OptionF, OptionId, OptionId>();
122        assert_eq!(result, None);
123    }
124
125    #[test]
126    fn fmap_day() {
127        let day = Day::<OptionF, OptionF, i32, i32, i32>::new(Some(2), Some(5), |a, b| a + b);
128        let mapped = day.fmap(|x| x * 3);
129        let result = mapped.run_day::<OptionF, OptionId, OptionId>();
130        assert_eq!(result, Some(21)); // (2+5)*3
131    }
132
133    #[test]
134    fn fmap_identity() {
135        let day = Day::<OptionF, OptionF, i32, i32, i32>::new(Some(7), Some(3), |a, b| a - b);
136        let mapped = day.fmap(|x| x);
137        let result = mapped.run_day::<OptionF, OptionId, OptionId>();
138        assert_eq!(result, Some(4));
139    }
140
141    #[test]
142    fn fmap_composition() {
143        let f = |x: i32| x + 1;
144        let g = |x: i32| x * 2;
145
146        let left = Day::<OptionF, OptionF, i32, i32, i32>::new(Some(3), Some(4), |a, b| a + b)
147            .fmap(move |a| g(f(a)));
148        let right = Day::<OptionF, OptionF, i32, i32, i32>::new(Some(3), Some(4), |a, b| a + b)
149            .fmap(f)
150            .fmap(g);
151
152        assert_eq!(
153            left.run_day::<OptionF, OptionId, OptionId>(),
154            right.run_day::<OptionF, OptionId, OptionId>()
155        );
156    }
157
158    #[test]
159    fn type_changing_combine() {
160        let day = Day::<OptionF, OptionF, String, i32, &str>::new(
161            Some(42),
162            Some("hello"),
163            |n: i32, s: &str| format!("{s}={n}"),
164        );
165        let result = day.run_day::<OptionF, OptionId, OptionId>();
166        assert_eq!(result, Some("hello=42".to_string()));
167    }
168
169    #[test]
170    fn multiple_fmaps() {
171        let day = Day::<OptionF, OptionF, i32, i32, i32>::new(Some(1), Some(2), |a, b| a + b)
172            .fmap(|x| x * 10)
173            .fmap(|x| x + 5);
174        let result = day.run_day::<OptionF, OptionId, OptionId>();
175        assert_eq!(result, Some(35)); // (1+2)*10+5
176    }
177}
178
179#[cfg(test)]
180mod law_tests {
181    use super::*;
182    use karpal_core::hkt::OptionF;
183    use proptest::prelude::*;
184
185    struct OptionId;
186    impl NaturalTransformation<OptionF, OptionF> for OptionId {
187        fn transform<A>(fa: Option<A>) -> Option<A> {
188            fa
189        }
190    }
191
192    proptest! {
193        // Functor identity: fmap(id).run_day() == run_day()
194        #[test]
195        fn functor_identity(a in any::<i32>(), b in any::<i32>()) {
196            let original = Day::<OptionF, OptionF, i32, i32, i32>::new(
197                Some(a), Some(b), |x, y| x.wrapping_add(y)
198            ).run_day::<OptionF, OptionId, OptionId>();
199            let mapped = Day::<OptionF, OptionF, i32, i32, i32>::new(
200                Some(a), Some(b), |x, y| x.wrapping_add(y)
201            ).fmap(|x| x).run_day::<OptionF, OptionId, OptionId>();
202            prop_assert_eq!(original, mapped);
203        }
204
205        // Functor composition: fmap(g . f) == fmap(f) . fmap(g)
206        #[test]
207        fn functor_composition(a in any::<i32>(), b in any::<i32>()) {
208            let f = |x: i32| x.wrapping_add(1);
209            let g = |x: i32| x.wrapping_mul(2);
210
211            let left = Day::<OptionF, OptionF, i32, i32, i32>::new(
212                Some(a), Some(b), |x, y| x.wrapping_add(y)
213            ).fmap(move |x| g(f(x))).run_day::<OptionF, OptionId, OptionId>();
214            let right = Day::<OptionF, OptionF, i32, i32, i32>::new(
215                Some(a), Some(b), |x, y| x.wrapping_add(y)
216            ).fmap(f).fmap(g).run_day::<OptionF, OptionId, OptionId>();
217            prop_assert_eq!(left, right);
218        }
219    }
220}