Skip to main content

karpal_core/
chain.rs

1// Copyright (C) 2026 Industrial Algebra
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::apply::Apply;
5use crate::hkt::OptionF;
6use crate::hkt::ResultF;
7#[cfg(any(feature = "std", feature = "alloc"))]
8use crate::hkt::VecF;
9#[cfg(all(not(feature = "std"), feature = "alloc"))]
10use alloc::vec::Vec;
11
12/// Chain (FlatMap): an Apply with monadic bind.
13///
14/// Laws:
15/// - Associativity: `chain(chain(m, f), g) == chain(m, |x| chain(f(x), g))`
16pub trait Chain: Apply {
17    fn chain<A, B>(fa: Self::Of<A>, f: impl Fn(A) -> Self::Of<B>) -> Self::Of<B>;
18}
19
20impl Chain for OptionF {
21    fn chain<A, B>(fa: Option<A>, f: impl Fn(A) -> Option<B>) -> Option<B> {
22        fa.and_then(f)
23    }
24}
25
26impl<E> Chain for ResultF<E> {
27    fn chain<A, B>(fa: Result<A, E>, f: impl Fn(A) -> Result<B, E>) -> Result<B, E> {
28        fa.and_then(f)
29    }
30}
31
32#[cfg(any(feature = "std", feature = "alloc"))]
33impl Chain for VecF {
34    fn chain<A, B>(fa: Vec<A>, f: impl Fn(A) -> Vec<B>) -> Vec<B> {
35        fa.into_iter().flat_map(f).collect()
36    }
37}
38
39impl Chain for crate::hkt::IdentityF {
40    fn chain<A, B>(fa: A, f: impl Fn(A) -> B) -> B {
41        f(fa)
42    }
43}
44
45#[cfg(any(feature = "std", feature = "alloc"))]
46impl Chain for crate::hkt::NonEmptyVecF {
47    fn chain<A, B>(
48        fa: crate::hkt::NonEmptyVec<A>,
49        f: impl Fn(A) -> crate::hkt::NonEmptyVec<B>,
50    ) -> crate::hkt::NonEmptyVec<B> {
51        let first = f(fa.head);
52        let mut tail = first.tail;
53        for a in fa.tail {
54            let result = f(a);
55            tail.push(result.head);
56            tail.extend(result.tail);
57        }
58        crate::hkt::NonEmptyVec::new(first.head, tail)
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn option_chain_some() {
68        let result = OptionF::chain(Some(3), |x| if x > 0 { Some(x * 2) } else { None });
69        assert_eq!(result, Some(6));
70    }
71
72    #[test]
73    fn option_chain_none() {
74        let result = OptionF::chain(None::<i32>, |x| Some(x * 2));
75        assert_eq!(result, None);
76    }
77
78    #[test]
79    fn result_chain_ok() {
80        let result = ResultF::<&str>::chain(Ok(3), |x| Ok(x + 1));
81        assert_eq!(result, Ok(4));
82    }
83
84    #[test]
85    fn result_chain_err() {
86        let result = ResultF::<&str>::chain(Err("bad"), |x: i32| Ok(x + 1));
87        assert_eq!(result, Err("bad"));
88    }
89
90    #[test]
91    fn vec_chain() {
92        let result = VecF::chain(vec![1, 2, 3], |x| vec![x, x * 10]);
93        assert_eq!(result, vec![1, 10, 2, 20, 3, 30]);
94    }
95}
96
97#[cfg(test)]
98mod law_tests {
99    use super::*;
100    use proptest::prelude::*;
101
102    proptest! {
103        // Associativity: chain(chain(m, f), g) == chain(m, |x| chain(f(x), g))
104        #[test]
105        fn option_associativity(x in any::<i16>()) {
106            let m = Some(x);
107            let f = |a: i16| Some(a.wrapping_add(1));
108            let g = |a: i16| Some(a.wrapping_mul(2));
109
110            let left = OptionF::chain(OptionF::chain(m, f), g);
111            let right = OptionF::chain(m, |a| OptionF::chain(f(a), g));
112            prop_assert_eq!(left, right);
113        }
114
115        #[test]
116        fn vec_associativity(x in prop::collection::vec(any::<i8>(), 0..5)) {
117            let f = |a: i8| vec![a, a.wrapping_add(1)];
118            let g = |a: i8| vec![a.wrapping_mul(2)];
119
120            let left = VecF::chain(VecF::chain(x.clone(), f), g);
121            let right = VecF::chain(x, |a| VecF::chain(f(a), g));
122            prop_assert_eq!(left, right);
123        }
124    }
125}