fp_library/types/free.rs
1use crate::{
2 Apply,
3 classes::{Functor, Runnable},
4 kinds::*,
5 types::cat_list::CatList,
6};
7use std::{any::Any, marker::PhantomData};
8
9/// A type-erased value for internal use.
10///
11/// This type alias represents a value whose type has been erased to `Box<dyn Any>`.
12/// It is used within the internal implementation of `Free` to allow for
13/// heterogeneous chains of computations in the [`CatList`].
14type Val = Box<dyn Any>;
15
16/// A type-erased continuation.
17///
18/// This type alias represents a function that takes a type-erased value [`Val`]
19/// and returns a new `Free` computation (also type-erased).
20///
21/// ### Type Parameters
22///
23/// * `F`: The base functor.
24type Cont<F> = Box<dyn FnOnce(Val) -> Free<F, Val>>;
25
26/// The internal representation of the `Free` monad.
27///
28/// This enum encodes the structure of the free monad, supporting
29/// pure values, suspended computations, and efficient concatenation of binds.
30///
31/// ### Type Parameters
32///
33/// * `F`: The base functor (must implement [`Functor`]).
34/// * `A`: The result type.
35///
36enum FreeInner<F, A>
37where
38 F: Functor + 'static,
39 A: 'static,
40{
41 /// A pure value.
42 ///
43 /// This variant represents a computation that has finished and produced a value.
44 Pure(A),
45
46 /// A suspended computation.
47 ///
48 /// This variant represents a computation that is suspended in the functor `F`.
49 /// The functor contains the next step of the computation.
50 Roll(Apply!(<F as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'static, Free<F, A>>)),
51
52 /// A bind operation.
53 ///
54 /// This variant represents a computation followed by a sequence of continuations.
55 /// It uses a [`CatList`] to store continuations, ensuring O(1) append complexity
56 /// for left-associated binds.
57 ///
58 /// ### Fields
59 ///
60 /// * `head`: The initial computation.
61 /// * `conts`: The list of continuations to apply to the result of `head`.
62 Bind { head: Box<Free<F, Val>>, conts: CatList<Cont<F>>, _marker: PhantomData<A> },
63}
64
65/// The Free monad with O(1) bind via CatList.
66///
67/// This implementation follows "Reflection without Remorse" to ensure
68/// that left-associated binds do not degrade performance.
69///
70/// # HKT and Lifetime Limitations
71///
72/// `Free` does not implement HKT traits (like `Functor`, `Monad`) from this library.
73///
74/// ## The Conflict
75/// * **The Traits**: The `Kind` trait implemented by the `Functor` hierarchy requires the type
76/// constructor to accept *any* lifetime `'a` (e.g., `type Of<'a, A> = Free<F, A>`).
77/// * **The Implementation**: This implementation uses `Box<dyn Any>` to type-erase continuations
78/// for the "Reflection without Remorse" optimization. `dyn Any` strictly requires `A: 'static`.
79///
80/// This creates an unresolvable conflict: `Free` cannot support non-static references (like `&'a str`),
81/// so it cannot satisfy the `Kind` signature.
82///
83/// ## Why not use the "Naive" Recursive Definition?
84///
85/// A naive definition (`enum Free { Pure(A), Roll(F<Box<Free<F, A>>>) }`) would support lifetimes
86/// and HKT traits. However, it was rejected because:
87/// 1. **Stack Safety**: `run` would not be stack-safe for deep computations.
88/// 2. **Performance**: `bind` would be O(N), leading to quadratic complexity for sequences of binds.
89///
90/// This implementation prioritizes **stack safety** and **O(1) bind** over HKT trait compatibility.
91///
92/// ### Type Parameters
93///
94/// * `F`: The base functor (must implement [`Functor`]).
95/// * `A`: The result type.
96///
97/// ### Examples
98///
99/// ```
100/// use fp_library::{brands::*, types::*};
101///
102/// let free = Free::<ThunkBrand, _>::pure(42);
103/// ```
104pub struct Free<F, A>(Option<FreeInner<F, A>>)
105where
106 F: Functor + 'static,
107 A: 'static;
108
109impl<F, A> Free<F, A>
110where
111 F: Functor + 'static,
112 A: 'static,
113{
114 /// Creates a pure Free value.
115 ///
116 /// ### Type Signature
117 ///
118 /// `forall f a. a -> Free f a`
119 ///
120 /// ### Parameters
121 ///
122 /// * `a`: The value to wrap.
123 ///
124 /// ### Returns
125 ///
126 /// A `Free` computation that produces `a`.
127 ///
128 /// ### Examples
129 ///
130 /// ```
131 /// use fp_library::{brands::*, types::*};
132 ///
133 /// let free = Free::<ThunkBrand, _>::pure(42);
134 /// ```
135 #[inline]
136 pub fn pure(a: A) -> Self {
137 Free(Some(FreeInner::Pure(a)))
138 }
139
140 /// Creates a suspended computation from a functor value.
141 ///
142 /// ### Type Signature
143 ///
144 /// `forall f a. f (Free f a) -> Free f a`
145 ///
146 /// ### Parameters
147 ///
148 /// * `fa`: The functor value containing the next step.
149 ///
150 /// ### Returns
151 ///
152 /// A `Free` computation that performs the effect `fa`.
153 ///
154 /// ### Examples
155 ///
156 /// ```
157 /// use fp_library::{brands::*, types::*};
158 ///
159 /// let eval = Thunk::new(|| Free::pure(42));
160 /// let free = Free::<ThunkBrand, _>::roll(eval);
161 /// ```
162 pub fn roll(
163 fa: Apply!(<F as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'static, Free<F, A>>)
164 ) -> Self {
165 Free(Some(FreeInner::Roll(fa)))
166 }
167
168 /// Monadic bind (flatMap) with O(1) complexity.
169 ///
170 /// ### Type Signature
171 ///
172 /// `forall f b a. (a -> Free f b, Free f a) -> Free f b`
173 ///
174 /// ### Type Parameters
175 ///
176 /// * `B`: The result type of the new computation.
177 ///
178 /// ### Parameters
179 ///
180 /// * `f`: The function to apply to the result of this computation.
181 ///
182 /// ### Returns
183 ///
184 /// A new `Free` computation that chains `f` after this computation.
185 ///
186 /// ### Examples
187 ///
188 /// ```
189 /// use fp_library::{brands::*, types::*};
190 ///
191 /// let free = Free::<ThunkBrand, _>::pure(42)
192 /// .bind(|x| Free::pure(x + 1));
193 /// ```
194 pub fn bind<B: 'static>(
195 mut self,
196 f: impl FnOnce(A) -> Free<F, B> + 'static,
197 ) -> Free<F, B> {
198 // Type-erase the continuation
199 let erased_f: Cont<F> = Box::new(move |val: Val| {
200 let a: A = *val.downcast().expect("Type mismatch in Free::bind");
201 let free_b: Free<F, B> = f(a);
202 free_b.erase_type()
203 });
204
205 // Extract inner safely
206 let inner = self.0.take().expect("Free value already consumed");
207
208 match inner {
209 // Pure: create a Bind with this continuation
210 FreeInner::Pure(a) => {
211 let head: Free<F, Val> = Free::pure(a).erase_type();
212 Free(Some(FreeInner::Bind {
213 head: Box::new(head),
214 conts: CatList::singleton(erased_f),
215 _marker: PhantomData,
216 }))
217 }
218
219 // Roll: wrap in a Bind
220 FreeInner::Roll(fa) => {
221 let head = Free::roll(fa).erase_type_boxed();
222 Free(Some(FreeInner::Bind {
223 head,
224 conts: CatList::singleton(erased_f),
225 _marker: PhantomData,
226 }))
227 }
228
229 // Bind: snoc the new continuation onto the CatList (O(1)!)
230 FreeInner::Bind { head, conts, .. } => Free(Some(FreeInner::Bind {
231 head,
232 conts: conts.snoc(erased_f),
233 _marker: PhantomData,
234 })),
235 }
236 }
237
238 /// Converts to type-erased form.
239 fn erase_type(mut self) -> Free<F, Val> {
240 let inner = self.0.take().expect("Free value already consumed");
241
242 match inner {
243 FreeInner::Pure(a) => Free(Some(FreeInner::Pure(Box::new(a) as Val))),
244 FreeInner::Roll(fa) => {
245 // Map over the functor to erase the inner type
246 let erased = F::map(|inner: Free<F, A>| inner.erase_type(), fa);
247 Free(Some(FreeInner::Roll(erased)))
248 }
249 FreeInner::Bind { head, conts, .. } => {
250 Free(Some(FreeInner::Bind { head, conts, _marker: PhantomData }))
251 }
252 }
253 }
254
255 /// Converts to boxed type-erased form.
256 fn erase_type_boxed(self) -> Box<Free<F, Val>> {
257 Box::new(self.erase_type())
258 }
259
260 /// Executes the Free computation, returning the final result.
261 ///
262 /// This is the "trampoline" that iteratively processes the
263 /// CatList of continuations without growing the stack.
264 ///
265 /// ### Type Signature
266 ///
267 /// `forall f a. Runnable f => Free f a -> a`
268 ///
269 /// ### Returns
270 ///
271 /// The final result of the computation.
272 ///
273 /// ### Examples
274 ///
275 /// ```
276 /// use fp_library::{brands::*, types::*};
277 ///
278 /// let free = Free::<ThunkBrand, _>::pure(42);
279 /// assert_eq!(free.run(), 42);
280 /// ```
281 pub fn run(self) -> A
282 where
283 F: Runnable,
284 {
285 // Start with a type-erased version
286 let mut current: Free<F, Val> = self.erase_type();
287 let mut conts: CatList<Cont<F>> = CatList::empty();
288
289 loop {
290 let inner = current.0.take().expect("Free value already consumed");
291
292 match inner {
293 FreeInner::Pure(val) => {
294 // Try to apply the next continuation
295 match conts.uncons() {
296 Some((cont, rest)) => {
297 current = cont(val);
298 conts = rest;
299 }
300 None => {
301 // No more continuations - we're done!
302 return *val
303 .downcast::<A>()
304 .expect("Type mismatch in Free::run final downcast");
305 }
306 }
307 }
308
309 FreeInner::Roll(fa) => {
310 // Run the effect to get the inner Free
311 current = <F as Runnable>::run(fa);
312 }
313
314 FreeInner::Bind { head, conts: inner_conts, .. } => {
315 // Merge the inner continuations with outer ones
316 // This is where CatList's O(1) append shines!
317 current = *head;
318 conts = inner_conts.append(conts);
319 }
320 }
321 }
322 }
323}
324
325impl<F, A> Drop for Free<F, A>
326where
327 F: Functor + 'static,
328 A: 'static,
329{
330 fn drop(&mut self) {
331 // We take the inner value out.
332 let inner = self.0.take();
333
334 // If the top level is a Bind, we need to start the iterative drop chain.
335 if let Some(FreeInner::Bind { mut head, .. }) = inner {
336 // head is Box<Free<F, Val>>.
337 // We take its inner value to continue the chain.
338 // From now on, everything is typed as FreeInner<F, Val>.
339 let mut current = head.0.take();
340
341 while let Some(FreeInner::Bind { mut head, .. }) = current {
342 current = head.0.take();
343 }
344 }
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use crate::{brands::ThunkBrand, types::thunk::Thunk};
352
353 /// Tests `Free::pure`.
354 ///
355 /// **What it tests:** Verifies that `pure` creates a computation that simply returns the provided value.
356 /// **How it tests:** Constructs a `Free::pure(42)` and runs it, asserting the result is 42.
357 #[test]
358 fn test_free_pure() {
359 let free = Free::<ThunkBrand, _>::pure(42);
360 assert_eq!(free.run(), 42);
361 }
362
363 /// Tests `Free::roll`.
364 ///
365 /// **What it tests:** Verifies that `roll` creates a computation from a suspended effect.
366 /// **How it tests:** Wraps a `Free::pure(42)` inside a `Thunk`, rolls it into a `Free`, and runs it to ensure it unwraps correctly.
367 #[test]
368 fn test_free_roll() {
369 let eval = Thunk::new(|| Free::pure(42));
370 let free = Free::<ThunkBrand, _>::roll(eval);
371 assert_eq!(free.run(), 42);
372 }
373
374 /// Tests `Free::bind`.
375 ///
376 /// **What it tests:** Verifies that `bind` correctly chains computations and passes values between them.
377 /// **How it tests:** Chains `pure(42) -> bind(+1) -> bind(*2)` and asserts the result is (42+1)*2 = 86.
378 #[test]
379 fn test_free_bind() {
380 let free =
381 Free::<ThunkBrand, _>::pure(42).bind(|x| Free::pure(x + 1)).bind(|x| Free::pure(x * 2));
382 assert_eq!(free.run(), 86);
383 }
384
385 /// Tests stack safety of `Free::run`.
386 ///
387 /// **What it tests:** Verifies that `run` can handle deep recursion without stack overflow (trampolining).
388 /// **How it tests:** Creates a recursive `count_down` function that builds a chain of 100,000 `bind` calls.
389 /// If the implementation were not stack-safe, this would crash with a stack overflow.
390 #[test]
391 fn test_free_stack_safety() {
392 fn count_down(n: i32) -> Free<ThunkBrand, i32> {
393 if n == 0 { Free::pure(0) } else { Free::pure(n).bind(|n| count_down(n - 1)) }
394 }
395
396 // 100,000 iterations should overflow stack if not safe
397 let free = count_down(100_000);
398 assert_eq!(free.run(), 0);
399 }
400
401 /// Tests stack safety of `Free::drop`.
402 ///
403 /// **What it tests:** Verifies that dropping a deep `Free` computation does not cause a stack overflow.
404 /// **How it tests:** Constructs a deep `Free` chain (similar to `test_free_stack_safety`) and lets it go out of scope.
405 #[test]
406 fn test_free_drop_safety() {
407 fn count_down(n: i32) -> Free<ThunkBrand, i32> {
408 if n == 0 { Free::pure(0) } else { Free::pure(n).bind(|n| count_down(n - 1)) }
409 }
410
411 // Construct a deep chain but DO NOT run it.
412 // When `free` goes out of scope, `Drop` should handle it iteratively.
413 let _free = count_down(100_000);
414 }
415
416 /// Tests `Free::bind` on a `Roll` variant.
417 ///
418 /// **What it tests:** Verifies that `bind` works correctly when applied to a suspended computation (`Roll`).
419 /// **How it tests:** Creates a `Roll` (via `roll`) and `bind`s it.
420 #[test]
421 fn test_free_bind_on_roll() {
422 let eval = Thunk::new(|| Free::pure(42));
423 let free = Free::<ThunkBrand, _>::roll(eval).bind(|x| Free::pure(x + 1));
424 assert_eq!(free.run(), 43);
425 }
426}