Skip to main content

karpal_free/
free.rs

1#[cfg(feature = "std")]
2use std::boxed::Box;
3
4#[cfg(all(not(feature = "std"), feature = "alloc"))]
5use alloc::boxed::Box;
6
7use core::marker::PhantomData;
8
9use karpal_core::applicative::Applicative;
10use karpal_core::chain::Chain;
11use karpal_core::functor::Functor;
12use karpal_core::hkt::HKT;
13use karpal_core::natural::NaturalTransformation;
14
15/// Free Monad — builds a monadic computation as a data structure.
16///
17/// `Free<F, A>` represents a program where `F` describes the available
18/// effects and `A` is the result type. Programs are built with `pure`
19/// and `lift_f`, composed with `chain`, and interpreted with `fold_map`
20/// using a natural transformation into any target monad.
21///
22/// ```text
23/// Pure(a)              — a finished computation returning a
24/// Roll(F<Free<F, A>>)  — one layer of effect wrapping a continuation
25/// ```
26pub enum Free<F: HKT, A> {
27    /// A pure value — the computation is finished.
28    Pure(A),
29    /// A layer of effect `F` wrapping a continuation.
30    Roll(Box<F::Of<Free<F, A>>>),
31}
32
33impl<F: HKT, A> Free<F, A> {
34    /// Wrap a pure value into the free monad.
35    pub fn pure(a: A) -> Self {
36        Free::Pure(a)
37    }
38}
39
40impl<F: HKT + Functor, A> Free<F, A> {
41    /// Lift a single effect `F<A>` into the free monad.
42    pub fn lift_f(fa: F::Of<A>) -> Self {
43        Free::Roll(Box::new(F::fmap(fa, Free::Pure)))
44    }
45
46    /// Map a function over the result of this computation.
47    pub fn fmap<B>(self, f: impl Fn(A) -> B) -> Free<F, B> {
48        self.fmap_inner(&f)
49    }
50
51    fn fmap_inner<B>(self, f: &dyn Fn(A) -> B) -> Free<F, B> {
52        match self {
53            Free::Pure(a) => Free::Pure(f(a)),
54            Free::Roll(ff) => Free::Roll(Box::new(F::fmap(*ff, |child| child.fmap_inner(f)))),
55        }
56    }
57
58    /// Monadic bind — sequence this computation with a function that
59    /// produces the next computation.
60    pub fn chain<B>(self, f: impl Fn(A) -> Free<F, B>) -> Free<F, B> {
61        self.chain_inner(&f)
62    }
63
64    fn chain_inner<B>(self, f: &dyn Fn(A) -> Free<F, B>) -> Free<F, B> {
65        match self {
66            Free::Pure(a) => f(a),
67            Free::Roll(ff) => Free::Roll(Box::new(F::fmap(*ff, |child| child.chain_inner(f)))),
68        }
69    }
70
71    /// Interpret this free monad into a target monad `M` using a natural
72    /// transformation `NT: F ~> M`.
73    ///
74    /// This is the core interpreter: it collapses the free structure by
75    /// translating each `F` effect into `M` and sequencing with `M::chain`.
76    pub fn fold_map<M, NT>(self) -> M::Of<A>
77    where
78        M: Applicative + Chain,
79        NT: NaturalTransformation<F, M>,
80    {
81        match self {
82            Free::Pure(a) => M::pure(a),
83            Free::Roll(ff) => {
84                let mapped = F::fmap(*ff, |child| child.fold_map::<M, NT>());
85                let m_ma: M::Of<M::Of<A>> = NT::transform(mapped);
86                M::chain(m_ma, |x| x)
87            }
88        }
89    }
90}
91
92/// HKT marker for `Free<F, _>`.
93pub struct FreeF<F: HKT>(PhantomData<F>);
94
95impl<F: HKT> HKT for FreeF<F> {
96    type Of<T> = Free<F, T>;
97}
98
99impl<F: HKT + Functor> Functor for FreeF<F> {
100    fn fmap<A, B>(fa: Free<F, A>, f: impl Fn(A) -> B) -> Free<F, B> {
101        fa.fmap(f)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use karpal_core::hkt::OptionF;
109
110    #[test]
111    fn pure_value() {
112        let free = Free::<OptionF, i32>::pure(42);
113        match free {
114            Free::Pure(v) => assert_eq!(v, 42),
115            Free::Roll(_) => panic!("expected Pure"),
116        }
117    }
118
119    #[test]
120    fn lift_f_some() {
121        let free = Free::<OptionF, i32>::lift_f(Some(1));
122        match free {
123            Free::Roll(ff) => match *ff {
124                Some(Free::Pure(v)) => assert_eq!(v, 1),
125                _ => panic!("expected Some(Pure(1))"),
126            },
127            Free::Pure(_) => panic!("expected Roll"),
128        }
129    }
130
131    #[test]
132    fn fmap_pure() {
133        let free = Free::<OptionF, i32>::pure(2).fmap(|x| x * 3);
134        match free {
135            Free::Pure(v) => assert_eq!(v, 6),
136            Free::Roll(_) => panic!("expected Pure"),
137        }
138    }
139
140    #[test]
141    fn fmap_roll() {
142        let free = Free::<OptionF, i32>::lift_f(Some(5)).fmap(|x| x + 10);
143        match free {
144            Free::Roll(ff) => match *ff {
145                Some(Free::Pure(v)) => assert_eq!(v, 15),
146                _ => panic!("expected Some(Pure(15))"),
147            },
148            Free::Pure(_) => panic!("expected Roll"),
149        }
150    }
151
152    #[test]
153    fn chain_pure() {
154        let free = Free::<OptionF, i32>::pure(1).chain(|x| Free::pure(x + 1));
155        match free {
156            Free::Pure(v) => assert_eq!(v, 2),
157            Free::Roll(_) => panic!("expected Pure"),
158        }
159    }
160
161    #[test]
162    fn chain_roll() {
163        let free = Free::<OptionF, i32>::lift_f(Some(10)).chain(|x| Free::pure(x * 2));
164        // Roll(Some(Pure(10))).chain(f)
165        // = Roll(fmap(Some(Pure(10)), |child| child.chain(f)))
166        // = Roll(Some(Pure(10).chain(f)))
167        // = Roll(Some(f(10)))
168        // = Roll(Some(Pure(20)))
169        match free {
170            Free::Roll(ff) => match *ff {
171                Some(Free::Pure(v)) => assert_eq!(v, 20),
172                _ => panic!("expected Some(Pure(20))"),
173            },
174            Free::Pure(_) => panic!("expected Roll"),
175        }
176    }
177
178    #[test]
179    fn chain_associativity() {
180        let _m = Free::<OptionF, i32>::pure(5);
181        let _f = |x: i32| Free::<OptionF, i32>::pure(x + 1);
182        let _g = |x: i32| Free::<OptionF, i32>::pure(x * 2);
183
184        // m.chain(f).chain(g)
185        let left = Free::<OptionF, i32>::pure(5)
186            .chain(|x| Free::pure(x + 1))
187            .chain(|x| Free::pure(x * 2));
188
189        // m.chain(|x| f(x).chain(g))
190        let right = Free::<OptionF, i32>::pure(5)
191            .chain(|x| Free::<OptionF, i32>::pure(x + 1).chain(|y| Free::pure(y * 2)));
192
193        match (left, right) {
194            (Free::Pure(l), Free::Pure(r)) => assert_eq!(l, r),
195            _ => panic!("expected both Pure"),
196        }
197    }
198
199    // Natural transformation: Option ~> Option (identity)
200    struct OptionId;
201    impl NaturalTransformation<OptionF, OptionF> for OptionId {
202        fn transform<A>(fa: Option<A>) -> Option<A> {
203            fa
204        }
205    }
206
207    #[test]
208    fn fold_map_pure() {
209        let free = Free::<OptionF, i32>::pure(42);
210        let result = free.fold_map::<OptionF, OptionId>();
211        assert_eq!(result, Some(42));
212    }
213
214    #[test]
215    fn fold_map_roll() {
216        let free = Free::<OptionF, i32>::lift_f(Some(10));
217        let result = free.fold_map::<OptionF, OptionId>();
218        assert_eq!(result, Some(10));
219    }
220
221    #[test]
222    fn fold_map_chain_then_interpret() {
223        let free = Free::<OptionF, i32>::lift_f(Some(3)).chain(|x| Free::lift_f(Some(x * 10)));
224        let result = free.fold_map::<OptionF, OptionId>();
225        assert_eq!(result, Some(30));
226    }
227
228    #[test]
229    fn functor_impl_works() {
230        let free = Free::<OptionF, i32>::pure(5);
231        let result = <FreeF<OptionF> as Functor>::fmap(free, |x| x + 10);
232        match result {
233            Free::Pure(v) => assert_eq!(v, 15),
234            Free::Roll(_) => panic!("expected Pure"),
235        }
236    }
237}
238
239#[cfg(test)]
240mod law_tests {
241    use super::*;
242    use karpal_core::hkt::OptionF;
243    use proptest::prelude::*;
244
245    // Helper to extract Pure value for comparison
246    fn extract_pure<F: HKT, A>(free: Free<F, A>) -> Option<A> {
247        match free {
248            Free::Pure(a) => Some(a),
249            Free::Roll(_) => None,
250        }
251    }
252
253    proptest! {
254        // Functor identity: fmap(id, fa) == fa
255        #[test]
256        fn functor_identity(x in any::<i32>()) {
257            let free = Free::<OptionF, i32>::pure(x);
258            let result = free.fmap(|a| a);
259            prop_assert_eq!(extract_pure(result), Some(x));
260        }
261
262        // Functor composition: fmap(g . f, fa) == fmap(g, fmap(f, fa))
263        #[test]
264        fn functor_composition(x in any::<i32>()) {
265            let f = |a: i32| a.wrapping_add(1);
266            let g = |a: i32| a.wrapping_mul(2);
267
268            let left = Free::<OptionF, i32>::pure(x).fmap(|a| g(f(a)));
269            let right = Free::<OptionF, i32>::pure(x).fmap(f).fmap(g);
270            prop_assert_eq!(extract_pure(left), extract_pure(right));
271        }
272
273        // Monad left identity: pure(a).chain(f) == f(a)
274        #[test]
275        fn monad_left_identity(x in any::<i32>()) {
276            let f = |a: i32| Free::<OptionF, i32>::pure(a.wrapping_mul(2));
277            let left = Free::<OptionF, i32>::pure(x).chain(&f);
278            let right = f(x);
279            prop_assert_eq!(extract_pure(left), extract_pure(right));
280        }
281
282        // Monad right identity: m.chain(pure) == m
283        #[test]
284        fn monad_right_identity(x in any::<i32>()) {
285            let m = Free::<OptionF, i32>::pure(x);
286            let result = m.chain(Free::pure);
287            prop_assert_eq!(extract_pure(result), Some(x));
288        }
289    }
290}