1#[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
22pub 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 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 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 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
88pub 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)); }
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)); }
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 #[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 #[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}