Skip to main content

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