Skip to main content

karpal_core/
functor.rs

1// Copyright (C) 2026 Industrial Algebra
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::hkt::{EnvF, HKT, IdentityF, OptionF, ResultF};
5#[cfg(any(feature = "std", feature = "alloc"))]
6use crate::hkt::{NonEmptyVec, NonEmptyVecF};
7#[cfg(all(not(feature = "std"), feature = "alloc"))]
8use alloc::vec::Vec;
9
10/// Covariant functor: lifts a function `A -> B` into `F<A> -> F<B>`.
11pub trait Functor: HKT {
12    fn fmap<A, B>(fa: Self::Of<A>, f: impl Fn(A) -> B) -> Self::Of<B>;
13}
14
15impl Functor for OptionF {
16    fn fmap<A, B>(fa: Option<A>, f: impl Fn(A) -> B) -> Option<B> {
17        fa.map(f)
18    }
19}
20
21impl<E> Functor for ResultF<E> {
22    fn fmap<A, B>(fa: Result<A, E>, f: impl Fn(A) -> B) -> Result<B, E> {
23        fa.map(f)
24    }
25}
26
27#[cfg(any(feature = "std", feature = "alloc"))]
28impl Functor for crate::hkt::VecF {
29    fn fmap<A, B>(fa: Vec<A>, f: impl Fn(A) -> B) -> Vec<B> {
30        fa.into_iter().map(f).collect()
31    }
32}
33
34impl Functor for IdentityF {
35    fn fmap<A, B>(fa: A, f: impl Fn(A) -> B) -> B {
36        f(fa)
37    }
38}
39
40#[cfg(any(feature = "std", feature = "alloc"))]
41impl Functor for NonEmptyVecF {
42    fn fmap<A, B>(fa: NonEmptyVec<A>, f: impl Fn(A) -> B) -> NonEmptyVec<B> {
43        NonEmptyVec::new(f(fa.head), fa.tail.into_iter().map(&f).collect())
44    }
45}
46
47impl<E> Functor for EnvF<E> {
48    fn fmap<A, B>(fa: (E, A), f: impl Fn(A) -> B) -> (E, B) {
49        (fa.0, f(fa.1))
50    }
51}
52
53// Note: StoreF and TracedF cannot implement the generic Functor trait because
54// Box<dyn Fn> requires 'static bounds that the trait signature doesn't allow.
55// They get their own fmap via the Extend/Comonad implementation.
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn option_fmap_some() {
63        let result = OptionF::fmap(Some(2), |x| x * 3);
64        assert_eq!(result, Some(6));
65    }
66
67    #[test]
68    fn option_fmap_none() {
69        let result = OptionF::fmap(None::<i32>, |x| x * 3);
70        assert_eq!(result, None);
71    }
72
73    #[test]
74    fn result_fmap_ok() {
75        let result = ResultF::<String>::fmap(Ok(5), |x| x + 1);
76        assert_eq!(result, Ok(6));
77    }
78
79    #[test]
80    fn result_fmap_err() {
81        let result = ResultF::<String>::fmap(Err("bad".to_string()), |x: i32| x + 1);
82        assert_eq!(result, Err("bad".to_string()));
83    }
84
85    #[test]
86    fn vec_fmap() {
87        let result = crate::hkt::VecF::fmap(vec![1, 2, 3], |x| x * 2);
88        assert_eq!(result, vec![2, 4, 6]);
89    }
90}
91
92#[cfg(test)]
93mod law_tests {
94    use super::*;
95    use proptest::prelude::*;
96
97    // Identity law: fmap(id, fa) == fa
98    // Composition law: fmap(g . f, fa) == fmap(g, fmap(f, fa))
99
100    proptest! {
101        #[test]
102        fn option_identity(x in any::<Option<i32>>()) {
103            let result = OptionF::fmap(x.clone(), |a| a);
104            prop_assert_eq!(result, x);
105        }
106
107        #[test]
108        fn option_composition(x in any::<Option<i32>>()) {
109            let f = |a: i32| a.wrapping_add(1);
110            let g = |a: i32| a.wrapping_mul(2);
111            let left = OptionF::fmap(x.clone(), |a| g(f(a)));
112            let right = OptionF::fmap(OptionF::fmap(x, f), g);
113            prop_assert_eq!(left, right);
114        }
115
116        #[test]
117        fn result_identity(x in any::<Result<i32, u8>>()) {
118            let result = ResultF::<u8>::fmap(x.clone(), |a| a);
119            prop_assert_eq!(result, x);
120        }
121
122        #[test]
123        fn result_composition(x in any::<Result<i32, u8>>()) {
124            let f = |a: i32| a.wrapping_add(1);
125            let g = |a: i32| a.wrapping_mul(2);
126            let left = ResultF::<u8>::fmap(x.clone(), |a| g(f(a)));
127            let right = ResultF::<u8>::fmap(ResultF::<u8>::fmap(x, f), g);
128            prop_assert_eq!(left, right);
129        }
130
131        #[test]
132        fn vec_identity(x in prop::collection::vec(any::<i32>(), 0..20)) {
133            let result = crate::hkt::VecF::fmap(x.clone(), |a| a);
134            prop_assert_eq!(result, x);
135        }
136
137        #[test]
138        fn vec_composition(x in prop::collection::vec(any::<i32>(), 0..20)) {
139            let f = |a: i32| a.wrapping_add(1);
140            let g = |a: i32| a.wrapping_mul(2);
141            let left = crate::hkt::VecF::fmap(x.clone(), |a| g(f(a)));
142            let right = crate::hkt::VecF::fmap(crate::hkt::VecF::fmap(x, f), g);
143            prop_assert_eq!(left, right);
144        }
145    }
146}