1#[cfg(feature = "std")]
5use std::boxed::Box;
6
7#[cfg(all(not(feature = "std"), feature = "alloc"))]
8use alloc::boxed::Box;
9
10use core::marker::PhantomData;
11
12use karpal_core::hkt::HKT;
13
14trait DensityDyn<W: HKT + 'static, A: 'static> {
18 fn extract_dyn(&self) -> A;
20}
21
22#[allow(clippy::type_complexity)]
24struct DensityCell<W: HKT + 'static, A: 'static, S: 'static> {
25 extract_fn: Box<dyn Fn(&W::Of<S>) -> A>,
26 source: W::Of<S>,
27 _marker: PhantomData<W>,
28}
29
30impl<W: HKT + 'static, A: 'static, S: 'static> DensityDyn<W, A> for DensityCell<W, A, S> {
31 fn extract_dyn(&self) -> A {
32 (self.extract_fn)(&self.source)
33 }
34}
35
36struct DensityMap<W: HKT + 'static, Src: 'static, A: 'static> {
38 inner: Box<dyn DensityDyn<W, Src>>,
39 transform: Box<dyn Fn(Src) -> A>,
40}
41
42impl<W: HKT + 'static, Src: 'static, A: 'static> DensityDyn<W, A> for DensityMap<W, Src, A> {
43 fn extract_dyn(&self) -> A {
44 (self.transform)(self.inner.extract_dyn())
45 }
46}
47
48pub struct Density<W: HKT + 'static, A: 'static> {
63 inner: Box<dyn DensityDyn<W, A>>,
64}
65
66impl<W: HKT + 'static, A: 'static> Density<W, A> {
67 pub fn lift<S: 'static>(source: W::Of<S>, f: impl Fn(&W::Of<S>) -> A + 'static) -> Self
69 where
70 W::Of<S>: 'static,
71 {
72 Density {
73 inner: Box::new(DensityCell {
74 extract_fn: Box::new(f),
75 source,
76 _marker: PhantomData,
77 }),
78 }
79 }
80
81 pub fn extract(&self) -> A {
83 self.inner.extract_dyn()
84 }
85
86 pub fn fmap<B: 'static>(self, f: impl Fn(A) -> B + 'static) -> Density<W, B> {
88 Density {
89 inner: Box::new(DensityMap {
90 inner: self.inner,
91 transform: Box::new(f),
92 }),
93 }
94 }
95}
96
97pub struct DensityF<W: HKT + 'static>(PhantomData<W>);
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use karpal_core::hkt::OptionF;
107
108 #[test]
109 fn lift_and_extract() {
110 let d = Density::<OptionF, i32>::lift(Some(42), |opt| opt.unwrap());
111 assert_eq!(d.extract(), 42);
112 }
113
114 #[test]
115 fn lift_extract_none() {
116 let d = Density::<OptionF, i32>::lift(None::<i32>, |_opt| 0);
117 assert_eq!(d.extract(), 0);
118 }
119
120 #[test]
121 fn fmap_density() {
122 let d = Density::<OptionF, i32>::lift(Some(5), |opt| opt.unwrap()).fmap(|x| x * 3);
123 assert_eq!(d.extract(), 15);
124 }
125
126 #[test]
127 fn fmap_identity() {
128 let d = Density::<OptionF, i32>::lift(Some(7), |opt| opt.unwrap()).fmap(|x| x);
129 assert_eq!(d.extract(), 7);
130 }
131
132 #[test]
133 fn fmap_composition() {
134 let f = |x: i32| x + 1;
135 let g = |x: i32| x * 2;
136
137 let left = Density::<OptionF, i32>::lift(Some(5), |opt| opt.unwrap())
138 .fmap(move |a| g(f(a)))
139 .extract();
140 let right = Density::<OptionF, i32>::lift(Some(5), |opt| opt.unwrap())
141 .fmap(f)
142 .fmap(g)
143 .extract();
144 assert_eq!(left, right);
145 }
146
147 #[test]
148 fn fmap_changes_type() {
149 let d = Density::<OptionF, i32>::lift(Some(42), |opt| opt.unwrap())
150 .fmap(|x| format!("val={x}"));
151 assert_eq!(d.extract(), "val=42");
152 }
153
154 #[test]
155 fn multiple_fmaps() {
156 let d = Density::<OptionF, i32>::lift(Some(1), |opt| opt.unwrap())
157 .fmap(|x| x + 1)
158 .fmap(|x| x * 10)
159 .fmap(|x| x + 5);
160 assert_eq!(d.extract(), 25); }
162
163 #[test]
164 fn extract_multiple_times() {
165 let d = Density::<OptionF, i32>::lift(Some(99), |opt| opt.unwrap());
166 assert_eq!(d.extract(), 99);
167 assert_eq!(d.extract(), 99);
168 }
169}
170
171#[cfg(test)]
172mod law_tests {
173 use super::*;
174 use karpal_core::hkt::OptionF;
175 use proptest::prelude::*;
176
177 proptest! {
178 #[test]
180 fn functor_identity(x in any::<i32>()) {
181 let original = Density::<OptionF, i32>::lift(Some(x), |opt| opt.unwrap()).extract();
182 let mapped = Density::<OptionF, i32>::lift(Some(x), |opt| opt.unwrap())
183 .fmap(|a| a)
184 .extract();
185 prop_assert_eq!(original, mapped);
186 }
187
188 #[test]
190 fn functor_composition(x in any::<i32>()) {
191 let f = |a: i32| a.wrapping_add(1);
192 let g = |a: i32| a.wrapping_mul(2);
193
194 let left = Density::<OptionF, i32>::lift(Some(x), |opt| opt.unwrap())
195 .fmap(move |a| g(f(a)))
196 .extract();
197 let right = Density::<OptionF, i32>::lift(Some(x), |opt| opt.unwrap())
198 .fmap(f)
199 .fmap(g)
200 .extract();
201 prop_assert_eq!(left, right);
202 }
203 }
204}