#[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, IdentityF};
use karpal_core::natural::NaturalTransformation;
use crate::coyoneda::Coyoneda;
pub struct Lan<G: HKT, H: HKT, A, B> {
extract_fn: Box<dyn Fn(G::Of<B>) -> A>,
source: H::Of<B>,
_marker: PhantomData<G>,
}
impl<G: HKT + 'static, H: HKT + 'static, A: 'static, B: 'static> Lan<G, H, A, B> {
pub fn new(source: H::Of<B>, f: impl Fn(G::Of<B>) -> A + 'static) -> Self {
Lan {
extract_fn: Box::new(f),
source,
_marker: PhantomData,
}
}
pub fn fmap<C: 'static>(self, f: impl Fn(A) -> C + 'static) -> Lan<G, H, C, B> {
let old_extract = self.extract_fn;
Lan {
extract_fn: Box::new(move |gb| f(old_extract(gb))),
source: self.source,
_marker: PhantomData,
}
}
pub fn lower<NT: NaturalTransformation<H, G>>(self) -> A {
(self.extract_fn)(NT::transform(self.source))
}
}
impl<H: HKT + 'static, A: 'static, B: 'static> Lan<IdentityF, H, A, B>
where
H::Of<B>: 'static,
{
pub fn to_coyoneda(self) -> Coyoneda<H, A, B> {
let extract = self.extract_fn;
Coyoneda::<H, _, _>::lift(self.source).fmap(move |b| (*extract)(b))
}
}
pub struct LanF<G: HKT, H: HKT, B>(PhantomData<(G, H, B)>);
#[cfg(test)]
mod tests {
use super::*;
use karpal_core::hkt::OptionF;
#[test]
fn new_and_lower() {
struct OptToId;
impl NaturalTransformation<OptionF, IdentityF> for OptToId {
fn transform<A>(fa: Option<A>) -> A {
fa.unwrap()
}
}
let lan = Lan::<IdentityF, OptionF, i32, i32>::new(Some(42), |x| x);
let result = lan.lower::<OptToId>();
assert_eq!(result, 42);
}
#[test]
fn fmap_lan() {
struct OptToId;
impl NaturalTransformation<OptionF, IdentityF> for OptToId {
fn transform<A>(fa: Option<A>) -> A {
fa.unwrap()
}
}
let lan = Lan::<IdentityF, OptionF, i32, i32>::new(Some(5), |x| x);
let mapped = lan.fmap(|x| x * 3);
let result = mapped.lower::<OptToId>();
assert_eq!(result, 15);
}
#[test]
fn fmap_composition() {
struct OptToId;
impl NaturalTransformation<OptionF, IdentityF> for OptToId {
fn transform<A>(fa: Option<A>) -> A {
fa.unwrap()
}
}
let f = |x: i32| x + 1;
let g = |x: i32| x * 2;
let left = Lan::<IdentityF, OptionF, i32, i32>::new(Some(5), |x| x).fmap(move |a| g(f(a)));
let right = Lan::<IdentityF, OptionF, i32, i32>::new(Some(5), |x| x)
.fmap(f)
.fmap(g);
assert_eq!(left.lower::<OptToId>(), right.lower::<OptToId>());
}
#[test]
fn fmap_identity() {
struct OptToId;
impl NaturalTransformation<OptionF, IdentityF> for OptToId {
fn transform<A>(fa: Option<A>) -> A {
fa.unwrap()
}
}
let lan = Lan::<IdentityF, OptionF, i32, i32>::new(Some(7), |x| x);
let mapped = lan.fmap(|x| x);
assert_eq!(mapped.lower::<OptToId>(), 7);
}
#[test]
fn to_coyoneda_roundtrip() {
let lan =
Lan::<IdentityF, OptionF, String, i32>::new(Some(42), |x: i32| format!("val={x}"));
let coy = lan.to_coyoneda();
let result = coy.lower();
assert_eq!(result, Some("val=42".to_string()));
}
#[test]
fn to_coyoneda_with_fmap() {
let lan = Lan::<IdentityF, OptionF, i32, i32>::new(Some(10), |x| x);
let coy = lan.fmap(|x| x + 5).to_coyoneda();
let result = coy.lower();
assert_eq!(result, Some(15));
}
#[test]
fn lower_with_extract_transform() {
struct OptionId;
impl NaturalTransformation<OptionF, OptionF> for OptionId {
fn transform<A>(fa: Option<A>) -> Option<A> {
fa
}
}
let lan =
Lan::<OptionF, OptionF, String, i32>::new(Some(99), |opt| format!("got: {:?}", opt));
let result = lan.lower::<OptionId>();
assert_eq!(result, "got: Some(99)");
}
}
#[cfg(test)]
mod law_tests {
use super::*;
use karpal_core::hkt::OptionF;
use proptest::prelude::*;
struct OptToId;
impl NaturalTransformation<OptionF, IdentityF> for OptToId {
fn transform<A>(fa: Option<A>) -> A {
fa.unwrap()
}
}
proptest! {
#[test]
fn functor_identity(x in any::<i32>()) {
let result = Lan::<IdentityF, OptionF, i32, i32>::new(Some(x), |a| a)
.fmap(|a| a)
.lower::<OptToId>();
prop_assert_eq!(result, x);
}
#[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 = Lan::<IdentityF, OptionF, i32, i32>::new(Some(x), |a| a)
.fmap(move |a| g(f(a)))
.lower::<OptToId>();
let right = Lan::<IdentityF, OptionF, i32, i32>::new(Some(x), |a| a)
.fmap(f)
.fmap(g)
.lower::<OptToId>();
prop_assert_eq!(left, right);
}
}
}