Skip to main content

karpal_effect/
except_t.rs

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