Skip to main content

karpal_free/
day.rs

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