Skip to main content

karpal_core/
extend.rs

1use crate::functor::Functor;
2use crate::hkt::{EnvF, IdentityF, OptionF};
3#[cfg(any(feature = "std", feature = "alloc"))]
4use crate::hkt::{NonEmptyVec, NonEmptyVecF};
5
6/// Extend: the dual of Chain. Enables cooperative "context-aware" computation.
7///
8/// Given a value in context `W<A>` and a function `&W<A> -> B` that can
9/// inspect the full context, `extend` applies that function at every
10/// "position" in the structure, producing `W<B>`.
11///
12/// Laws:
13/// - Associativity: `extend(f, extend(g, w)) == extend(|w| f(&extend(g, w.clone())), w)`
14pub trait Extend: Functor {
15    fn extend<A, B>(wa: Self::Of<A>, f: impl Fn(&Self::Of<A>) -> B) -> Self::Of<B>
16    where
17        A: Clone;
18
19    fn duplicate<A>(wa: Self::Of<A>) -> Self::Of<Self::Of<A>>
20    where
21        A: Clone,
22        Self::Of<A>: Clone,
23    {
24        Self::extend(wa, |w| w.clone())
25    }
26}
27
28impl Extend for IdentityF {
29    fn extend<A, B>(wa: A, f: impl Fn(&A) -> B) -> B
30    where
31        A: Clone,
32    {
33        f(&wa)
34    }
35}
36
37impl Extend for OptionF {
38    fn extend<A, B>(wa: Option<A>, f: impl Fn(&Option<A>) -> B) -> Option<B>
39    where
40        A: Clone,
41    {
42        if wa.is_some() { Some(f(&wa)) } else { None }
43    }
44}
45
46#[cfg(any(feature = "std", feature = "alloc"))]
47impl Extend for NonEmptyVecF {
48    fn extend<A, B>(wa: NonEmptyVec<A>, f: impl Fn(&NonEmptyVec<A>) -> B) -> NonEmptyVec<B>
49    where
50        A: Clone,
51    {
52        // Apply f to each suffix of the NonEmptyVec
53        let suffixes = wa.tails();
54        let head = f(&suffixes.head);
55        let tail = suffixes.tail.iter().map(&f).collect();
56        NonEmptyVec::new(head, tail)
57    }
58}
59
60impl<E> Extend for EnvF<E> {
61    fn extend<A, B>(wa: (E, A), f: impl Fn(&(E, A)) -> B) -> (E, B)
62    where
63        A: Clone,
64    {
65        let b = f(&wa);
66        (wa.0, b)
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn identity_extend() {
76        let result = IdentityF::extend(42, |w| w + 1);
77        assert_eq!(result, 43);
78    }
79
80    #[test]
81    fn option_extend_some() {
82        let result = OptionF::extend(Some(3), |opt| match opt {
83            Some(x) => x * 2,
84            None => 0,
85        });
86        assert_eq!(result, Some(6));
87    }
88
89    #[test]
90    fn option_extend_none() {
91        let result = OptionF::extend(None::<i32>, |opt| match opt {
92            Some(x) => x * 2,
93            None => 0,
94        });
95        assert_eq!(result, None);
96    }
97
98    #[test]
99    fn nonemptyvec_extend() {
100        let nev = NonEmptyVec::new(1, vec![2, 3]);
101        // Sum of each suffix
102        let result = NonEmptyVecF::extend(nev, |w| w.iter().sum::<i32>());
103        // Suffixes: [1,2,3], [2,3], [3]
104        // Sums: 6, 5, 3
105        assert_eq!(result, NonEmptyVec::new(6, vec![5, 3]));
106    }
107
108    #[test]
109    fn env_extend() {
110        let result = EnvF::<&str>::extend(("hello", 42), |&(env, val)| format!("{}: {}", env, val));
111        assert_eq!(result, ("hello", "hello: 42".to_string()));
112    }
113
114    #[test]
115    fn identity_duplicate() {
116        let result = IdentityF::duplicate(42);
117        assert_eq!(result, 42);
118    }
119
120    #[test]
121    fn option_duplicate() {
122        let result = OptionF::duplicate(Some(42));
123        assert_eq!(result, Some(Some(42)));
124    }
125
126    #[test]
127    fn nonemptyvec_duplicate() {
128        let nev = NonEmptyVec::new(1, vec![2, 3]);
129        let result = NonEmptyVecF::duplicate(nev);
130        assert_eq!(result.head, NonEmptyVec::new(1, vec![2, 3]));
131        assert_eq!(result.tail.len(), 2);
132        assert_eq!(result.tail[0], NonEmptyVec::new(2, vec![3]));
133        assert_eq!(result.tail[1], NonEmptyVec::new(3, vec![]));
134    }
135}
136
137#[cfg(test)]
138mod law_tests {
139    use super::*;
140    use proptest::prelude::*;
141
142    fn nonemptyvec_strategy<T: core::fmt::Debug + Clone + 'static>(
143        elem: impl Strategy<Value = T> + Clone + 'static,
144    ) -> impl Strategy<Value = NonEmptyVec<T>> {
145        (elem.clone(), prop::collection::vec(elem, 0..5))
146            .prop_map(|(head, tail)| NonEmptyVec::new(head, tail))
147    }
148
149    proptest! {
150        // Associativity: extend(f, extend(g, w)) == extend(|w| f(&extend(g, w.clone())), w)
151        #[test]
152        fn option_associativity(x in any::<Option<i16>>()) {
153            let f = |opt: &Option<i16>| opt.map_or(0i16, |v| v.wrapping_add(1));
154            let g = |opt: &Option<i16>| opt.map_or(0i16, |v| v.wrapping_mul(2));
155
156            let left = OptionF::extend(OptionF::extend(x.clone(), g), f);
157            let right = OptionF::extend(x, |w| f(&OptionF::extend(w.clone(), g)));
158            prop_assert_eq!(left, right);
159        }
160
161        #[test]
162        fn nonemptyvec_associativity(w in nonemptyvec_strategy(any::<i8>())) {
163            let f = |nev: &NonEmptyVec<i8>| nev.head.wrapping_add(1);
164            let g = |nev: &NonEmptyVec<i8>| nev.head.wrapping_mul(2);
165
166            let left = NonEmptyVecF::extend(NonEmptyVecF::extend(w.clone(), g), f);
167            let right = NonEmptyVecF::extend(w, |w| f(&NonEmptyVecF::extend(w.clone(), g)));
168            prop_assert_eq!(left, right);
169        }
170    }
171}