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
19pub 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 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 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 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
85pub 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)); }
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)); }
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 #[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 #[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}