#[cfg(feature = "std")]
use std::boxed::Box;
#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::boxed::Box;
use core::marker::PhantomData;
use karpal_core::hkt::HKT;
trait DensityDyn<W: HKT + 'static, A: 'static> {
fn extract_dyn(&self) -> A;
}
#[allow(clippy::type_complexity)]
struct DensityCell<W: HKT + 'static, A: 'static, S: 'static> {
extract_fn: Box<dyn Fn(&W::Of<S>) -> A>,
source: W::Of<S>,
_marker: PhantomData<W>,
}
impl<W: HKT + 'static, A: 'static, S: 'static> DensityDyn<W, A> for DensityCell<W, A, S> {
fn extract_dyn(&self) -> A {
(self.extract_fn)(&self.source)
}
}
struct DensityMap<W: HKT + 'static, Src: 'static, A: 'static> {
inner: Box<dyn DensityDyn<W, Src>>,
transform: Box<dyn Fn(Src) -> A>,
}
impl<W: HKT + 'static, Src: 'static, A: 'static> DensityDyn<W, A> for DensityMap<W, Src, A> {
fn extract_dyn(&self) -> A {
(self.transform)(self.inner.extract_dyn())
}
}
pub struct Density<W: HKT + 'static, A: 'static> {
inner: Box<dyn DensityDyn<W, A>>,
}
impl<W: HKT + 'static, A: 'static> Density<W, A> {
pub fn lift<S: 'static>(source: W::Of<S>, f: impl Fn(&W::Of<S>) -> A + 'static) -> Self
where
W::Of<S>: 'static,
{
Density {
inner: Box::new(DensityCell {
extract_fn: Box::new(f),
source,
_marker: PhantomData,
}),
}
}
pub fn extract(&self) -> A {
self.inner.extract_dyn()
}
pub fn fmap<B: 'static>(self, f: impl Fn(A) -> B + 'static) -> Density<W, B> {
Density {
inner: Box::new(DensityMap {
inner: self.inner,
transform: Box::new(f),
}),
}
}
}
pub struct DensityF<W: HKT + 'static>(PhantomData<W>);
#[cfg(test)]
mod tests {
use super::*;
use karpal_core::hkt::OptionF;
#[test]
fn lift_and_extract() {
let d = Density::<OptionF, i32>::lift(Some(42), |opt| opt.unwrap());
assert_eq!(d.extract(), 42);
}
#[test]
fn lift_extract_none() {
let d = Density::<OptionF, i32>::lift(None::<i32>, |_opt| 0);
assert_eq!(d.extract(), 0);
}
#[test]
fn fmap_density() {
let d = Density::<OptionF, i32>::lift(Some(5), |opt| opt.unwrap()).fmap(|x| x * 3);
assert_eq!(d.extract(), 15);
}
#[test]
fn fmap_identity() {
let d = Density::<OptionF, i32>::lift(Some(7), |opt| opt.unwrap()).fmap(|x| x);
assert_eq!(d.extract(), 7);
}
#[test]
fn fmap_composition() {
let f = |x: i32| x + 1;
let g = |x: i32| x * 2;
let left = Density::<OptionF, i32>::lift(Some(5), |opt| opt.unwrap())
.fmap(move |a| g(f(a)))
.extract();
let right = Density::<OptionF, i32>::lift(Some(5), |opt| opt.unwrap())
.fmap(f)
.fmap(g)
.extract();
assert_eq!(left, right);
}
#[test]
fn fmap_changes_type() {
let d = Density::<OptionF, i32>::lift(Some(42), |opt| opt.unwrap())
.fmap(|x| format!("val={x}"));
assert_eq!(d.extract(), "val=42");
}
#[test]
fn multiple_fmaps() {
let d = Density::<OptionF, i32>::lift(Some(1), |opt| opt.unwrap())
.fmap(|x| x + 1)
.fmap(|x| x * 10)
.fmap(|x| x + 5);
assert_eq!(d.extract(), 25); }
#[test]
fn extract_multiple_times() {
let d = Density::<OptionF, i32>::lift(Some(99), |opt| opt.unwrap());
assert_eq!(d.extract(), 99);
assert_eq!(d.extract(), 99);
}
}
#[cfg(test)]
mod law_tests {
use super::*;
use karpal_core::hkt::OptionF;
use proptest::prelude::*;
proptest! {
#[test]
fn functor_identity(x in any::<i32>()) {
let original = Density::<OptionF, i32>::lift(Some(x), |opt| opt.unwrap()).extract();
let mapped = Density::<OptionF, i32>::lift(Some(x), |opt| opt.unwrap())
.fmap(|a| a)
.extract();
prop_assert_eq!(original, mapped);
}
#[test]
fn functor_composition(x in any::<i32>()) {
let f = |a: i32| a.wrapping_add(1);
let g = |a: i32| a.wrapping_mul(2);
let left = Density::<OptionF, i32>::lift(Some(x), |opt| opt.unwrap())
.fmap(move |a| g(f(a)))
.extract();
let right = Density::<OptionF, i32>::lift(Some(x), |opt| opt.unwrap())
.fmap(f)
.fmap(g)
.extract();
prop_assert_eq!(left, right);
}
}
}