Skip to main content

karpal_free/
density.rs

1// Copyright (C) 2026 Industrial Algebra
2// SPDX-License-Identifier: Apache-2.0
3
4#[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
14/// Dyn-safe trait for the existential encoding of Density.
15///
16/// Hides the state type `S` in `∃S. (W S → A, W S)`.
17trait DensityDyn<W: HKT + 'static, A: 'static> {
18    /// Extract the value by applying the stored function to the source.
19    fn extract_dyn(&self) -> A;
20}
21
22/// Concrete cell: stores `extract_fn: &W::Of<S> → A` and `source: W::Of<S>`.
23#[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
36/// Map wrapper: composes a function on top of extract.
37struct 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
48/// Density Comonad — the CPS dual of Codensity.
49///
50/// `Density<W, A> ≅ ∃S. (W S → A, W S)`
51///
52/// This is the left Kan extension of `W` along itself (`Lan W W A`),
53/// specialised into a concrete type with existential state.
54///
55/// # Key properties
56///
57/// - **`extract`**: Requires no bounds on `W` — just applies the stored function.
58/// - **`fmap`**: Composes onto the extract function, no bounds on `W`.
59///
60/// Note: Due to Rust's GAT limitations, `DensityF` does not implement
61/// `HKT` or `Comonad`. Use inherent methods instead.
62pub 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    /// Construct a Density from a source value and an extract function.
68    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    /// Extract the value. No bounds on `W` required.
82    pub fn extract(&self) -> A {
83        self.inner.extract_dyn()
84    }
85
86    /// Map a function over the result. No bounds on `W` required.
87    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
97/// Marker type for `Density<W, _>`.
98///
99/// Note: Cannot implement `HKT` or `Comonad` due to Rust's GAT limitations.
100/// Use `Density::extract`, `Density::fmap` directly.
101pub 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); // (1+1)*10+5
161    }
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        // Functor identity: fmap(id).extract() == extract()
179        #[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        // Functor composition: fmap(g . f) == fmap(f) . fmap(g)
189        #[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}