Skip to main content

karpal_core/
extend.rs

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