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