Skip to main content

karpal_effect/
writer_t.rs

1use core::marker::PhantomData;
2
3use karpal_core::hkt::HKT;
4use karpal_core::monoid::Monoid;
5use karpal_core::semigroup::Semigroup;
6
7use crate::classes::{ApplicativeSt, ChainSt, FunctorSt};
8use crate::trans::MonadTrans;
9
10/// WriterT monad transformer: adds log accumulation to an inner monad.
11///
12/// `WriterTF<W, M>::Of<A> = M::Of<(A, W)>`
13///
14/// The log type `W` must be a `Monoid` (for `pure`/`lift`) and a `Semigroup`
15/// (for `chain`, which combines logs). This is the strict (non-lazy) variant.
16pub struct WriterTF<W, M>(PhantomData<(W, M)>);
17
18impl<W: 'static, M: HKT> HKT for WriterTF<W, M> {
19    type Of<A> = M::Of<(A, W)>;
20}
21
22impl<W: Monoid + 'static, M: FunctorSt> MonadTrans<M> for WriterTF<W, M> {
23    fn lift<A: 'static>(ma: M::Of<A>) -> M::Of<(A, W)> {
24        M::fmap_st(ma, |a| (a, W::empty()))
25    }
26}
27
28/// WriterT `pure`: wrap a value with an empty log.
29pub fn writer_t_pure<W: Monoid + 'static, M: ApplicativeSt, A: 'static>(a: A) -> M::Of<(A, W)> {
30    M::pure_st((a, W::empty()))
31}
32
33/// WriterT `fmap`: apply a function to the value, preserving the log.
34pub fn writer_t_fmap<W: 'static, M: FunctorSt, A: 'static, B: 'static>(
35    fa: M::Of<(A, W)>,
36    f: impl Fn(A) -> B + 'static,
37) -> M::Of<(B, W)> {
38    M::fmap_st(fa, move |(a, w)| (f(a), w))
39}
40
41/// WriterT `chain`: sequence log-accumulating computations.
42///
43/// The logs from both computations are combined using `Semigroup::combine`.
44pub fn writer_t_chain<W: Semigroup + Clone + 'static, M: ChainSt, A: 'static, B: 'static>(
45    fa: M::Of<(A, W)>,
46    f: impl Fn(A) -> M::Of<(B, W)> + 'static,
47) -> M::Of<(B, W)> {
48    M::chain_st(fa, move |(a, w1)| {
49        M::fmap_st(f(a), move |(b, w2)| {
50            let w1_owned = w1.clone();
51            (b, w1_owned.combine(w2))
52        })
53    })
54}
55
56/// WriterT `tell`: append a log value, producing `()`.
57pub fn writer_t_tell<W: 'static, M: ApplicativeSt>(w: W) -> M::Of<((), W)> {
58    M::pure_st(((), w))
59}
60
61/// WriterT `listen`: expose the log alongside the value.
62pub fn writer_t_listen<W: Clone + 'static, M: FunctorSt, A: 'static>(
63    fa: M::Of<(A, W)>,
64) -> M::Of<((A, W), W)> {
65    M::fmap_st(fa, |(a, w): (A, W)| {
66        let w2 = w.clone();
67        ((a, w), w2)
68    })
69}
70
71/// WriterT `pass`: apply a function to the log.
72///
73/// The value is a pair `(a, f)` where `f` transforms the log.
74#[allow(clippy::type_complexity)]
75pub fn writer_t_pass<W: 'static, M: FunctorSt, A: 'static>(
76    fa: M::Of<((A, Box<dyn Fn(W) -> W>), W)>,
77) -> M::Of<(A, W)> {
78    M::fmap_st(fa, |((a, f), w)| (a, f(w)))
79}
80
81/// WriterT `run`: unwrap the transformer (identity — included for API symmetry).
82pub fn writer_t_run<W, M: HKT, A>(fa: M::Of<(A, W)>) -> M::Of<(A, W)> {
83    fa
84}
85
86// --- FunctorSt / ApplicativeSt / ChainSt for WriterTF ---
87
88impl<W: 'static, M: FunctorSt> FunctorSt for WriterTF<W, M> {
89    fn fmap_st<A: 'static, B: 'static>(
90        fa: M::Of<(A, W)>,
91        f: impl Fn(A) -> B + 'static,
92    ) -> M::Of<(B, W)> {
93        writer_t_fmap::<W, M, A, B>(fa, f)
94    }
95}
96
97impl<W: Monoid + 'static, M: ApplicativeSt> ApplicativeSt for WriterTF<W, M> {
98    fn pure_st<A: 'static>(a: A) -> M::Of<(A, W)> {
99        writer_t_pure::<W, M, A>(a)
100    }
101}
102
103impl<W: Semigroup + Clone + 'static, M: ChainSt + FunctorSt> ChainSt for WriterTF<W, M> {
104    fn chain_st<A: 'static, B: 'static>(
105        fa: M::Of<(A, W)>,
106        f: impl Fn(A) -> M::Of<(B, W)> + 'static,
107    ) -> M::Of<(B, W)> {
108        writer_t_chain::<W, M, A, B>(fa, f)
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use karpal_core::hkt::{IdentityF, OptionF};
116
117    #[test]
118    fn writer_t_pure_identity() {
119        let result = writer_t_pure::<String, IdentityF, i32>(42);
120        assert_eq!(result, (42, String::new()));
121    }
122
123    #[test]
124    fn writer_t_pure_option() {
125        let result = writer_t_pure::<String, OptionF, i32>(42);
126        assert_eq!(result, Some((42, String::new())));
127    }
128
129    #[test]
130    fn writer_t_fmap_test() {
131        let val = writer_t_pure::<String, OptionF, i32>(10);
132        let result = writer_t_fmap::<String, OptionF, _, _>(val, |x| x * 3);
133        assert_eq!(result, Some((30, String::new())));
134    }
135
136    #[test]
137    fn writer_t_tell_test() {
138        let told = writer_t_tell::<String, OptionF>("hello".to_string());
139        assert_eq!(told, Some(((), "hello".to_string())));
140    }
141
142    #[test]
143    fn writer_t_chain_accumulates_log() {
144        let m1 = writer_t_tell::<String, OptionF>("a".to_string());
145        let result = writer_t_chain::<String, OptionF, _, _>(m1, |()| {
146            writer_t_tell::<String, OptionF>("b".to_string())
147        });
148        assert_eq!(result, Some(((), "ab".to_string())));
149    }
150
151    #[test]
152    fn writer_t_chain_with_value() {
153        let m1: Option<(i32, String)> = Some((10, "start".to_string()));
154        let result =
155            writer_t_chain::<String, OptionF, _, _>(m1, |x| Some((x + 5, " end".to_string())));
156        assert_eq!(result, Some((15, "start end".to_string())));
157    }
158
159    #[test]
160    fn writer_t_chain_none() {
161        let m1: Option<(i32, String)> = None;
162        let result =
163            writer_t_chain::<String, OptionF, _, _>(m1, |x| Some((x + 5, "end".to_string())));
164        assert_eq!(result, None);
165    }
166
167    #[test]
168    fn writer_t_listen_test() {
169        let val: Option<(i32, String)> = Some((42, "log".to_string()));
170        let result = writer_t_listen::<String, OptionF, i32>(val);
171        assert_eq!(result, Some(((42, "log".to_string()), "log".to_string())));
172    }
173
174    #[test]
175    fn writer_t_pass_test() {
176        let f: Box<dyn Fn(String) -> String> = Box::new(|w| w.to_uppercase());
177        let val: Option<((i32, Box<dyn Fn(String) -> String>), String)> =
178            Some(((42, f), "hello".to_string()));
179        let result = writer_t_pass::<String, OptionF, i32>(val);
180        assert_eq!(result, Some((42, "HELLO".to_string())));
181    }
182
183    #[test]
184    fn writer_t_lift_option() {
185        let lifted = WriterTF::<String, OptionF>::lift(Some(42));
186        assert_eq!(lifted, Some((42, String::new())));
187    }
188
189    #[test]
190    fn writer_t_lift_none() {
191        let lifted = WriterTF::<String, OptionF>::lift(None::<i32>);
192        assert_eq!(lifted, None);
193    }
194
195    // Trait impls
196
197    #[test]
198    fn writer_t_functor_st_trait() {
199        let val = writer_t_pure::<String, OptionF, i32>(5);
200        let result = WriterTF::<String, OptionF>::fmap_st(val, |x| x + 1);
201        assert_eq!(result, Some((6, String::new())));
202    }
203
204    #[test]
205    fn writer_t_chain_st_trait() {
206        let val = WriterTF::<String, OptionF>::pure_st(5);
207        let result =
208            WriterTF::<String, OptionF>::chain_st(val, |x| Some((x + 10, "log".to_string())));
209        assert_eq!(result, Some((15, "log".to_string())));
210    }
211}
212
213#[cfg(test)]
214mod law_tests {
215    use super::*;
216    use karpal_core::hkt::OptionF;
217    use proptest::prelude::*;
218
219    proptest! {
220        // Functor identity
221        #[test]
222        fn writer_t_functor_identity(a in any::<i16>(), w in "[a-z]{0,5}") {
223            let val: Option<(i16, String)> = Some((a, w.clone()));
224            let left = writer_t_fmap::<String, OptionF, _, _>(val.clone(), |x| x);
225            prop_assert_eq!(left, val);
226        }
227
228        // Monad left identity: chain(pure(a), f) == f(a)
229        #[test]
230        fn writer_t_monad_left_identity(a in -100i32..100) {
231            let f = |x: i32| -> Option<(i32, String)> {
232                Some((x + 1, "f".to_string()))
233            };
234            let left = writer_t_chain::<String, OptionF, _, _>(
235                writer_t_pure::<String, OptionF, _>(a),
236                f,
237            );
238            let right = f(a);
239            prop_assert_eq!(left, right);
240        }
241
242        // Monad right identity: chain(m, pure) == m
243        #[test]
244        fn writer_t_monad_right_identity(a in any::<i16>(), w in "[a-z]{0,5}") {
245            let val: Option<(i16, String)> = Some((a, w));
246            let left = writer_t_chain::<String, OptionF, _, _>(
247                val.clone(),
248                |x| writer_t_pure::<String, OptionF, _>(x),
249            );
250            prop_assert_eq!(left, val);
251        }
252
253        // MonadTrans: lift(pure(a)) == pure(a)
254        #[test]
255        fn writer_t_lift_pure(a in any::<i32>()) {
256            let lift_pure = WriterTF::<String, OptionF>::lift(Some(a));
257            let pure_a = writer_t_pure::<String, OptionF, _>(a);
258            prop_assert_eq!(lift_pure, pure_a);
259        }
260    }
261}