Skip to main content

karpal_effect/
reader_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#[cfg(all(not(feature = "std"), feature = "alloc"))]
12use alloc::rc::Rc;
13#[cfg(feature = "std")]
14use std::rc::Rc;
15
16/// ReaderT monad transformer: adds environment-passing to an inner monad.
17///
18/// `ReaderTF<E, M>::Of<A> = Box<dyn Fn(E) -> M::Of<A>>`
19///
20/// When the inner monad is `IdentityF`, this is equivalent to `ReaderF<E>`.
21pub struct ReaderTF<E, M>(PhantomData<(E, M)>);
22
23impl<E: 'static, M: HKT + 'static> HKT for ReaderTF<E, M> {
24    type Of<A> = Box<dyn Fn(E) -> M::Of<A>>;
25}
26
27impl<E: 'static, M: FunctorSt + 'static> MonadTrans<M> for ReaderTF<E, M> {
28    fn lift<A: 'static>(ma: M::Of<A>) -> Box<dyn Fn(E) -> M::Of<A>>
29    where
30        M::Of<A>: Clone,
31    {
32        Box::new(move |_| ma.clone())
33    }
34}
35
36/// ReaderT `pure`: wrap a value, ignoring the environment.
37pub fn reader_t_pure<E: 'static, M: ApplicativeSt + 'static, A: Clone + 'static>(
38    a: A,
39) -> Box<dyn Fn(E) -> M::Of<A>> {
40    Box::new(move |_| M::pure_st(a.clone()))
41}
42
43/// ReaderT `fmap`: apply a function to the result.
44pub fn reader_t_fmap<E: 'static, M: FunctorSt + 'static, A: 'static, B: 'static>(
45    fa: Box<dyn Fn(E) -> M::Of<A>>,
46    f: impl Fn(A) -> B + 'static,
47) -> Box<dyn Fn(E) -> M::Of<B>> {
48    let f_rc = Rc::new(f);
49    Box::new(move |e| {
50        let ma = fa(e);
51        let f_inner = f_rc.clone();
52        M::fmap_st(ma, move |a| f_inner(a))
53    })
54}
55
56/// ReaderT `chain`: sequence environment-passing computations.
57///
58/// The environment is shared (not threaded) between computations.
59pub fn reader_t_chain<E: Clone + 'static, M: ChainSt + 'static, A: 'static, B: 'static>(
60    fa: Box<dyn Fn(E) -> M::Of<A>>,
61    f: impl Fn(A) -> Box<dyn Fn(E) -> M::Of<B>> + 'static,
62) -> Box<dyn Fn(E) -> M::Of<B>> {
63    let f_rc = Rc::new(f);
64    Box::new(move |e: E| {
65        let ma = fa(e.clone());
66        let e2 = e;
67        let f_inner = f_rc.clone();
68        M::chain_st(ma, move |a| {
69            let reader_b = f_inner(a);
70            reader_b(e2.clone())
71        })
72    })
73}
74
75/// ReaderT `ask`: get the current environment.
76pub fn reader_t_ask<E: Clone + 'static, M: ApplicativeSt + 'static>() -> Box<dyn Fn(E) -> M::Of<E>>
77{
78    Box::new(|e| M::pure_st(e))
79}
80
81/// ReaderT `local`: modify the environment for a sub-computation.
82pub fn reader_t_local<E: 'static, M: HKT + 'static, A: 'static>(
83    f: impl Fn(E) -> E + 'static,
84    reader: Box<dyn Fn(E) -> M::Of<A>>,
85) -> Box<dyn Fn(E) -> M::Of<A>> {
86    Box::new(move |e| reader(f(e)))
87}
88
89/// ReaderT `reader`: create a computation from a function on the environment.
90pub fn reader_t_reader<E: 'static, M: ApplicativeSt + 'static, A: 'static>(
91    f: impl Fn(E) -> A + 'static,
92) -> Box<dyn Fn(E) -> M::Of<A>> {
93    Box::new(move |e| M::pure_st(f(e)))
94}
95
96/// ReaderT `run`: run the computation with an environment.
97pub fn reader_t_run<E, M: HKT, A>(reader: &dyn Fn(E) -> M::Of<A>, env: E) -> M::Of<A> {
98    reader(env)
99}
100
101// --- FunctorSt / ChainSt for ReaderTF ---
102// Note: ApplicativeSt is not implemented for ReaderTF because pure_st
103// cannot produce a Box<dyn Fn(E) -> M::Of<A>> from a single A without Clone.
104// Use the standalone reader_t_pure function instead (which requires A: Clone).
105
106impl<E: 'static, M: FunctorSt + 'static> FunctorSt for ReaderTF<E, M> {
107    fn fmap_st<A: 'static, B: 'static>(
108        fa: Box<dyn Fn(E) -> M::Of<A>>,
109        f: impl Fn(A) -> B + 'static,
110    ) -> Box<dyn Fn(E) -> M::Of<B>> {
111        reader_t_fmap::<E, M, A, B>(fa, f)
112    }
113}
114
115impl<E: Clone + 'static, M: ChainSt + 'static> ChainSt for ReaderTF<E, M> {
116    fn chain_st<A: 'static, B: 'static>(
117        fa: Box<dyn Fn(E) -> M::Of<A>>,
118        f: impl Fn(A) -> Box<dyn Fn(E) -> M::Of<B>> + 'static,
119    ) -> Box<dyn Fn(E) -> M::Of<B>> {
120        reader_t_chain::<E, M, A, B>(fa, f)
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use karpal_core::hkt::OptionF;
128
129    #[test]
130    fn reader_t_pure_test() {
131        let r = reader_t_pure::<i32, OptionF, _>(42);
132        assert_eq!(r(0), Some(42));
133        assert_eq!(r(999), Some(42));
134    }
135
136    #[test]
137    fn reader_t_ask_test() {
138        let r = reader_t_ask::<i32, OptionF>();
139        assert_eq!(r(42), Some(42));
140    }
141
142    #[test]
143    fn reader_t_fmap_test() {
144        let r = reader_t_ask::<i32, OptionF>();
145        let mapped = reader_t_fmap::<i32, OptionF, _, _>(r, |x| x * 2);
146        assert_eq!(mapped(5), Some(10));
147    }
148
149    #[test]
150    fn reader_t_chain_test() {
151        let r = reader_t_ask::<i32, OptionF>();
152        let chained =
153            reader_t_chain::<i32, OptionF, _, _>(r, |x| reader_t_pure::<i32, OptionF, _>(x + 10));
154        assert_eq!(chained(5), Some(15));
155    }
156
157    #[test]
158    fn reader_t_chain_shares_env() {
159        let r = reader_t_chain::<i32, OptionF, _, _>(reader_t_ask::<i32, OptionF>(), |x| {
160            let x_captured = x;
161            reader_t_fmap::<i32, OptionF, _, _>(reader_t_ask::<i32, OptionF>(), move |e| {
162                e + x_captured
163            })
164        });
165        assert_eq!(r(10), Some(20));
166    }
167
168    #[test]
169    fn reader_t_local_test() {
170        let r = reader_t_ask::<i32, OptionF>();
171        let localized = reader_t_local::<i32, OptionF, i32>(|e| e + 100, r);
172        assert_eq!(localized(5), Some(105));
173    }
174
175    #[test]
176    fn reader_t_reader_test() {
177        let r = reader_t_reader::<String, OptionF, _>(|s: String| s.len());
178        assert_eq!(r("hello".to_string()), Some(5));
179    }
180
181    #[test]
182    fn reader_t_lift_test() {
183        let lifted = ReaderTF::<i32, OptionF>::lift(Some(42));
184        assert_eq!(lifted(999), Some(42));
185    }
186
187    #[test]
188    fn reader_t_lift_none() {
189        let lifted = ReaderTF::<i32, OptionF>::lift(None::<i32>);
190        assert_eq!(lifted(999), None);
191    }
192
193    #[test]
194    fn reader_t_run_test() {
195        let r = reader_t_ask::<i32, OptionF>();
196        assert_eq!(reader_t_run::<i32, OptionF, i32>(&*r, 42), Some(42));
197    }
198
199    #[test]
200    fn reader_t_functor_st_trait() {
201        let r = reader_t_pure::<i32, OptionF, _>(5);
202        let mapped = ReaderTF::<i32, OptionF>::fmap_st(r, |x| x + 1);
203        assert_eq!(mapped(0), Some(6));
204    }
205
206    #[test]
207    fn reader_t_chain_st_trait() {
208        let r = reader_t_pure::<i32, OptionF, _>(5);
209        let chained =
210            ReaderTF::<i32, OptionF>::chain_st(r, |x| reader_t_pure::<i32, OptionF, _>(x + 10));
211        assert_eq!(chained(0), Some(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        #[test]
223        fn reader_t_monad_left_identity(a in -100i32..100, e in -100i32..100) {
224            let f = |x: i32| -> Box<dyn Fn(i32) -> Option<i32>> {
225                reader_t_pure::<i32, OptionF, _>(x + 1)
226            };
227            let left = reader_t_chain::<i32, OptionF, _, _>(
228                reader_t_pure::<i32, OptionF, _>(a),
229                f,
230            );
231            let right = f(a);
232            prop_assert_eq!(left(e), right(e));
233        }
234
235        #[test]
236        fn reader_t_monad_right_identity(a in -100i32..100, e in -100i32..100) {
237            let m = reader_t_pure::<i32, OptionF, _>(a);
238            let left = reader_t_chain::<i32, OptionF, _, _>(
239                reader_t_pure::<i32, OptionF, _>(a),
240                |x| reader_t_pure::<i32, OptionF, _>(x),
241            );
242            prop_assert_eq!(left(e), m(e));
243        }
244
245        #[test]
246        fn reader_t_functor_identity(a in -100i32..100, e in -100i32..100) {
247            let m = reader_t_pure::<i32, OptionF, _>(a);
248            let mapped = reader_t_fmap::<i32, OptionF, _, _>(
249                reader_t_pure::<i32, OptionF, _>(a),
250                |x| x,
251            );
252            prop_assert_eq!(mapped(e), m(e));
253        }
254
255        #[test]
256        fn reader_t_lift_pure(a in any::<i32>(), e in any::<i32>()) {
257            let lift_pure = ReaderTF::<i32, OptionF>::lift(Some(a));
258            let pure_a = reader_t_pure::<i32, OptionF, _>(a);
259            prop_assert_eq!(lift_pure(e), pure_a(e));
260        }
261    }
262}