Skip to main content

karpal_free/
density.rs

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
11/// Dyn-safe trait for the existential encoding of Density.
12///
13/// Hides the state type `S` in `∃S. (W S → A, W S)`.
14trait DensityDyn<W: HKT + 'static, A: 'static> {
15    /// Extract the value by applying the stored function to the source.
16    fn extract_dyn(&self) -> A;
17}
18
19/// Concrete cell: stores `extract_fn: &W::Of<S> → A` and `source: W::Of<S>`.
20#[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
33/// Map wrapper: composes a function on top of extract.
34struct 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
45/// Density Comonad — the CPS dual of Codensity.
46///
47/// `Density<W, A> ≅ ∃S. (W S → A, W S)`
48///
49/// This is the left Kan extension of `W` along itself (`Lan W W A`),
50/// specialised into a concrete type with existential state.
51///
52/// # Key properties
53///
54/// - **`extract`**: Requires no bounds on `W` — just applies the stored function.
55/// - **`fmap`**: Composes onto the extract function, no bounds on `W`.
56///
57/// Note: Due to Rust's GAT limitations, `DensityF` does not implement
58/// `HKT` or `Comonad`. Use inherent methods instead.
59pub 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    /// Construct a Density from a source value and an extract function.
65    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    /// Extract the value. No bounds on `W` required.
79    pub fn extract(&self) -> A {
80        self.inner.extract_dyn()
81    }
82
83    /// Map a function over the result. No bounds on `W` required.
84    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
94/// Marker type for `Density<W, _>`.
95///
96/// Note: Cannot implement `HKT` or `Comonad` due to Rust's GAT limitations.
97/// Use `Density::extract`, `Density::fmap` directly.
98pub 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); // (1+1)*10+5
158    }
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        // Functor identity: fmap(id).extract() == extract()
176        #[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        // Functor composition: fmap(g . f) == fmap(f) . fmap(g)
186        #[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}