Skip to main content

karpal_effect/
except_t.rs

1use core::marker::PhantomData;
2
3use karpal_core::hkt::HKT;
4
5use crate::classes::{ApplicativeSt, ChainSt, FunctorSt};
6use crate::trans::MonadTrans;
7
8/// ExceptT monad transformer: adds error handling to an inner monad.
9///
10/// `ExceptTF<E, M>::Of<A> = M::Of<Result<A, E>>`
11///
12/// When the inner monad is `IdentityF`, this is equivalent to `ResultF<E>`.
13pub struct ExceptTF<E, M>(PhantomData<(E, M)>);
14
15impl<E: 'static, M: HKT> HKT for ExceptTF<E, M> {
16    type Of<A> = M::Of<Result<A, E>>;
17}
18
19impl<E: 'static, M: FunctorSt> MonadTrans<M> for ExceptTF<E, M> {
20    fn lift<A: 'static>(ma: M::Of<A>) -> M::Of<Result<A, E>> {
21        M::fmap_st(ma, Ok)
22    }
23}
24
25/// ExceptT `pure`: wrap a value in `Ok` inside the inner monad.
26pub fn except_t_pure<E: 'static, M: ApplicativeSt, A: 'static>(a: A) -> M::Of<Result<A, E>> {
27    M::pure_st(Ok(a))
28}
29
30/// ExceptT `fmap`: apply a function to the `Ok` value.
31pub fn except_t_fmap<E: 'static, M: FunctorSt, A: 'static, B: 'static>(
32    fa: M::Of<Result<A, E>>,
33    f: impl Fn(A) -> B + 'static,
34) -> M::Of<Result<B, E>> {
35    M::fmap_st(fa, move |r| r.map(&f))
36}
37
38/// ExceptT `chain`: sequence error-handling computations.
39///
40/// Short-circuits on `Err` — the function `f` is only called for `Ok` values.
41pub fn except_t_chain<E: 'static, M: ChainSt + ApplicativeSt, A: 'static, B: 'static>(
42    fa: M::Of<Result<A, E>>,
43    f: impl Fn(A) -> M::Of<Result<B, E>> + 'static,
44) -> M::Of<Result<B, E>> {
45    M::chain_st(fa, move |r| match r {
46        Ok(a) => f(a),
47        Err(e) => M::pure_st(Err(e)),
48    })
49}
50
51/// ExceptT `throw_error`: produce an error value inside the transformer.
52pub fn except_t_throw<E: 'static, M: ApplicativeSt, A: 'static>(e: E) -> M::Of<Result<A, E>> {
53    M::pure_st(Err(e))
54}
55
56/// ExceptT `catch_error`: handle an error by running a recovery function.
57pub fn except_t_catch<E: 'static, M: ChainSt + ApplicativeSt, A: 'static>(
58    fa: M::Of<Result<A, E>>,
59    handler: impl Fn(E) -> M::Of<Result<A, E>> + 'static,
60) -> M::Of<Result<A, E>> {
61    M::chain_st(fa, move |r| match r {
62        Ok(a) => M::pure_st(Ok(a)),
63        Err(e) => handler(e),
64    })
65}
66
67/// ExceptT `run`: unwrap the transformer (identity — included for API symmetry).
68pub fn except_t_run<E, M: HKT, A>(fa: M::Of<Result<A, E>>) -> M::Of<Result<A, E>> {
69    fa
70}
71
72// --- FunctorSt / ApplicativeSt / ChainSt for ExceptTF ---
73
74impl<E: 'static, M: FunctorSt> FunctorSt for ExceptTF<E, M> {
75    fn fmap_st<A: 'static, B: 'static>(
76        fa: M::Of<Result<A, E>>,
77        f: impl Fn(A) -> B + 'static,
78    ) -> M::Of<Result<B, E>> {
79        except_t_fmap::<E, M, A, B>(fa, f)
80    }
81}
82
83impl<E: 'static, M: ApplicativeSt> ApplicativeSt for ExceptTF<E, M> {
84    fn pure_st<A: 'static>(a: A) -> M::Of<Result<A, E>> {
85        except_t_pure::<E, M, A>(a)
86    }
87}
88
89impl<E: 'static, M: ChainSt + ApplicativeSt> ChainSt for ExceptTF<E, M> {
90    fn chain_st<A: 'static, B: 'static>(
91        fa: M::Of<Result<A, E>>,
92        f: impl Fn(A) -> M::Of<Result<B, E>> + 'static,
93    ) -> M::Of<Result<B, E>> {
94        except_t_chain::<E, M, A, B>(fa, f)
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use karpal_core::hkt::{IdentityF, OptionF};
102
103    #[test]
104    fn except_t_pure_identity() {
105        let result = except_t_pure::<&str, IdentityF, i32>(42);
106        assert_eq!(result, Ok(42));
107    }
108
109    #[test]
110    fn except_t_pure_option() {
111        let result = except_t_pure::<&str, OptionF, i32>(42);
112        assert_eq!(result, Some(Ok(42)));
113    }
114
115    #[test]
116    fn except_t_fmap_ok() {
117        let val = except_t_pure::<&str, OptionF, i32>(10);
118        let result = except_t_fmap::<&str, OptionF, _, _>(val, |x| x * 3);
119        assert_eq!(result, Some(Ok(30)));
120    }
121
122    #[test]
123    fn except_t_fmap_err() {
124        let val: Option<Result<i32, &str>> = Some(Err("bad"));
125        let result = except_t_fmap::<&str, OptionF, _, _>(val, |x: i32| x * 3);
126        assert_eq!(result, Some(Err("bad")));
127    }
128
129    #[test]
130    fn except_t_fmap_none() {
131        let val: Option<Result<i32, &str>> = None;
132        let result = except_t_fmap::<&str, OptionF, _, _>(val, |x: i32| x * 3);
133        assert_eq!(result, None);
134    }
135
136    #[test]
137    fn except_t_chain_ok() {
138        let val = except_t_pure::<&str, OptionF, i32>(5);
139        let result = except_t_chain::<&str, OptionF, _, _>(val, |x| Some(Ok(x + 10)));
140        assert_eq!(result, Some(Ok(15)));
141    }
142
143    #[test]
144    fn except_t_chain_err_short_circuits() {
145        let val: Option<Result<i32, &str>> = Some(Err("fail"));
146        let result = except_t_chain::<&str, OptionF, _, _>(val, |x| Some(Ok(x + 10)));
147        assert_eq!(result, Some(Err("fail")));
148    }
149
150    #[test]
151    fn except_t_chain_none() {
152        let val: Option<Result<i32, &str>> = None;
153        let result = except_t_chain::<&str, OptionF, _, _>(val, |x| Some(Ok(x + 10)));
154        assert_eq!(result, None);
155    }
156
157    #[test]
158    fn except_t_throw_test() {
159        let result = except_t_throw::<&str, OptionF, i32>("oops");
160        assert_eq!(result, Some(Err("oops")));
161    }
162
163    #[test]
164    fn except_t_catch_recovers() {
165        let val: Option<Result<i32, &str>> = Some(Err("bad"));
166        let result = except_t_catch::<&str, OptionF, i32>(val, |_| Some(Ok(42)));
167        assert_eq!(result, Some(Ok(42)));
168    }
169
170    #[test]
171    fn except_t_catch_ok_passes_through() {
172        let val = except_t_pure::<&str, OptionF, i32>(10);
173        let result = except_t_catch::<&str, OptionF, i32>(val, |_| Some(Ok(42)));
174        assert_eq!(result, Some(Ok(10)));
175    }
176
177    #[test]
178    fn except_t_lift_option() {
179        let lifted = ExceptTF::<&str, OptionF>::lift(Some(42));
180        assert_eq!(lifted, Some(Ok(42)));
181    }
182
183    #[test]
184    fn except_t_lift_none() {
185        let lifted = ExceptTF::<&str, OptionF>::lift(None::<i32>);
186        assert_eq!(lifted, None);
187    }
188
189    // Trait impls
190
191    #[test]
192    fn except_t_functor_st_trait() {
193        let val = except_t_pure::<&str, OptionF, i32>(5);
194        let result = ExceptTF::<&str, OptionF>::fmap_st(val, |x| x + 1);
195        assert_eq!(result, Some(Ok(6)));
196    }
197
198    #[test]
199    fn except_t_applicative_st_trait() {
200        let result = ExceptTF::<&str, OptionF>::pure_st(99);
201        assert_eq!(result, Some(Ok(99)));
202    }
203
204    #[test]
205    fn except_t_chain_st_trait() {
206        let val = ExceptTF::<&str, OptionF>::pure_st(5);
207        let result = ExceptTF::<&str, OptionF>::chain_st(val, |x| Some(Ok(x + 10)));
208        assert_eq!(result, Some(Ok(15)));
209    }
210}
211
212#[cfg(test)]
213mod law_tests {
214    use super::*;
215    use karpal_core::hkt::OptionF;
216    use proptest::prelude::*;
217
218    proptest! {
219        // Functor identity
220        #[test]
221        fn except_t_functor_identity(x in any::<Option<Result<i32, i32>>>()) {
222            let left = except_t_fmap::<i32, OptionF, _, _>(x.clone(), |a| a);
223            prop_assert_eq!(left, x);
224        }
225
226        // Functor composition
227        #[test]
228        fn except_t_functor_composition(x in any::<Option<Result<i16, i16>>>()) {
229            let f = |a: i16| a.wrapping_add(1);
230            let g = |a: i16| a.wrapping_mul(2);
231            let left = except_t_fmap::<i16, OptionF, _, _>(x.clone(), move |a| g(f(a)));
232            let right = except_t_fmap::<i16, OptionF, _, _>(
233                except_t_fmap::<i16, OptionF, _, _>(x, f),
234                g,
235            );
236            prop_assert_eq!(left, right);
237        }
238
239        // Monad left identity: chain(pure(a), f) == f(a)
240        #[test]
241        fn except_t_monad_left_identity(a in -100i32..100) {
242            let f = |x: i32| -> Option<Result<i32, &str>> { Some(Ok(x + 1)) };
243            let left = except_t_chain::<&str, OptionF, _, _>(
244                except_t_pure::<&str, OptionF, _>(a),
245                f,
246            );
247            let right = f(a);
248            prop_assert_eq!(left, right);
249        }
250
251        // Monad right identity: chain(m, pure) == m
252        #[test]
253        fn except_t_monad_right_identity(x in any::<Option<Result<i32, i32>>>()) {
254            let left = except_t_chain::<i32, OptionF, _, _>(
255                x.clone(),
256                |a| except_t_pure::<i32, OptionF, _>(a),
257            );
258            prop_assert_eq!(left, x);
259        }
260
261        // MonadTrans: lift(pure(a)) == pure(a)
262        #[test]
263        fn except_t_lift_pure(a in any::<i32>()) {
264            let lift_pure = ExceptTF::<&str, OptionF>::lift(Some(a));
265            let pure_a = except_t_pure::<&str, OptionF, _>(a);
266            prop_assert_eq!(lift_pure, pure_a);
267        }
268    }
269}