fp_library/types/free.rs
1//! Stack-safe Free monad over a functor with O(1) [`bind`](crate::functions::bind) operations.
2//!
3//! Enables building computation chains without stack overflow by using a catenable list of continuations. Note: requires `'static` types and cannot implement the library's HKT traits due to type erasure.
4//!
5//! ## Comparison with PureScript
6//!
7//! This implementation is based on the PureScript [`Control.Monad.Free`](https://github.com/purescript/purescript-free/blob/master/src/Control/Monad/Free.purs) module
8//! and the ["Reflection without Remorse"](http://okmij.org/ftp/Haskell/zseq.pdf) technique. It shares the same core algorithmic properties (O(1) bind, stack safety)
9//! but differs significantly in its intended use case and API surface.
10//!
11//! ### Key Differences
12//!
13//! 1. **Interpretation Strategy**:
14//! * **PureScript**: Designed as a generic Abstract Syntax Tree (AST) that can be interpreted into *any* target
15//! monad using `runFree` or `foldFree` by providing a natural transformation at runtime.
16//! * **Rust**: Designed primarily for **stack-safe execution** of computations. The interpretation logic is
17//! baked into the [`Evaluable`](crate::classes::Evaluable) trait implemented by the functor `F`.
18//! The [`Free::wrap`] method wraps a functor layer containing a Free computation.
19//!
20//! 2. **API Surface**:
21//! * **PureScript**: Rich API including `liftF`, `hoistFree`, `resume`, `foldFree`.
22//! * **Rust**: Focused API with construction (`pure`, `wrap`, `lift_f`, `bind`) and execution (`evaluate`).
23//! * `resume` is missing (cannot inspect the computation step-by-step).
24//! * `hoistFree` is missing.
25//!
26//! 3. **Terminology**:
27//! * Rust's `Free::wrap` corresponds to PureScript's `wrap`.
28//!
29//! ### Capabilities and Limitations
30//!
31//! **What it CAN do:**
32//! * Provide stack-safe recursion for monadic computations (trampolining).
33//! * Prevent stack overflows when chaining many `bind` operations.
34//! * Execute self-describing effects (like [`Thunk`](crate::types::Thunk)).
35//!
36//! **What it CANNOT do (easily):**
37//! * Act as a generic DSL where the interpretation is decoupled from the operation type.
38//! * *Example*: You cannot easily define a `DatabaseOp` enum and interpret it differently for
39//! production (SQL) and testing (InMemory) using this `Free` implementation, because
40//! `DatabaseOp` must implement a single `Runnable` trait.
41//! * Inspect the structure of the computation (introspection) via `resume`.
42//!
43//! ### Lifetimes and Memory Management
44//!
45//! * **PureScript**: Relies on a garbage collector and `unsafeCoerce`. This allows it to ignore
46//! lifetimes and ownership, enabling a simpler implementation that supports all types.
47//! * **Rust**: Relies on ownership and `Box<dyn Any>` for type erasure. `Any` requires `'static`
48//! to ensure memory safety (preventing use-after-free of references). This forces `Free` to
49//! only work with `'static` types, preventing it from implementing the library's HKT traits
50//! which require lifetime polymorphism.
51//!
52//! ### Examples
53//!
54//! ```
55//! use fp_library::{brands::*, types::*};
56//!
57//! // ✅ CAN DO: Stack-safe recursion
58//! let free = Free::<ThunkBrand, _>::pure(42)
59//! .bind(|x| Free::pure(x + 1));
60//! ```
61
62#[fp_macros::document_module]
63mod inner {
64 use crate::{
65 Apply,
66 brands::ThunkBrand,
67 classes::{Deferrable, Evaluable, Functor},
68 kinds::*,
69 types::{CatList, Thunk},
70 };
71 use fp_macros::{document_fields, document_parameters, document_type_parameters};
72 use std::{any::Any, marker::PhantomData};
73
74 /// A type-erased value for internal use.
75 ///
76 /// This type alias represents a value whose type has been erased to [`Box<dyn Any>`].
77 /// It is used within the internal implementation of [`Free`] to allow for
78 /// heterogeneous chains of computations in the [`CatList`].
79 pub type TypeErasedValue = Box<dyn Any>;
80
81 /// A type-erased continuation.
82 ///
83 /// This type alias represents a function that takes a [`TypeErasedValue`]
84 /// and returns a new [`Free`] computation (also type-erased).
85 ///
86 /// ### Type Parameters
87 ///
88 #[document_type_parameters("The base functor.")]
89 pub type Continuation<F> = Box<dyn FnOnce(TypeErasedValue) -> Free<F, TypeErasedValue>>;
90
91 /// The internal representation of the [`Free`] monad.
92 ///
93 /// This enum encodes the structure of the free monad, supporting
94 /// pure values, suspended computations, and efficient concatenation of binds.
95 ///
96 /// ### Type Parameters
97 ///
98 #[document_type_parameters(
99 "The base functor (must implement [`Functor`]).",
100 "The result type."
101 )]
102 #[document_fields]
103 pub enum FreeInner<F, A>
104 where
105 F: Functor + 'static,
106 A: 'static,
107 {
108 /// A pure value.
109 ///
110 /// This variant represents a computation that has finished and produced a value.
111 Pure(A),
112
113 /// A suspended computation.
114 ///
115 /// This variant represents a computation that is suspended in the functor `F`.
116 /// The functor contains the next step of the computation.
117 Wrap(Apply!(<F as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'static, Free<F, A>>)),
118
119 /// A bind operation.
120 ///
121 /// This variant represents a computation followed by a sequence of continuations.
122 /// It uses a [`CatList`] to store continuations, ensuring O(1) append complexity
123 /// for left-associated binds.
124 Bind {
125 /// The initial computation.
126 head: Box<Free<F, TypeErasedValue>>,
127 /// The list of continuations to apply to the result of `head`.
128 continuations: CatList<Continuation<F>>,
129 /// Phantom data for type parameter `A`.
130 _marker: PhantomData<A>,
131 },
132 }
133
134 /// The Free monad with O(1) bind via [`CatList`].
135 ///
136 /// This implementation follows ["Reflection without Remorse"](http://okmij.org/ftp/Haskell/zseq.pdf) to ensure
137 /// that left-associated binds do not degrade performance.
138 ///
139 /// # HKT and Lifetime Limitations
140 ///
141 /// `Free` does not implement HKT traits (like `Functor`, `Monad`) from this library.
142 ///
143 /// ## The Conflict
144 /// * **The Traits**: The `Kind` trait implemented by the `Functor` hierarchy requires the type
145 /// constructor to accept *any* lifetime `'a` (e.g., `type Of<'a, A> = Free<F, A>`).
146 /// * **The Implementation**: This implementation uses [`Box<dyn Any>`] to type-erase continuations
147 /// for the "Reflection without Remorse" optimization. `dyn Any` strictly requires `A: 'static`.
148 ///
149 /// This creates an unresolvable conflict: `Free` cannot support non-static references (like `&'a str`),
150 /// so it cannot satisfy the `Kind` signature.
151 ///
152 /// ## Why not use the "Naive" Recursive Definition?
153 ///
154 /// A naive definition (`enum Free { Pure(A), Wrap(F<Box<Free<F, A>>>) }`) would support lifetimes
155 /// and HKT traits. However, it was rejected because:
156 /// 1. **Stack Safety**: `run` would not be stack-safe for deep computations.
157 /// 2. **Performance**: `bind` would be O(N), leading to quadratic complexity for sequences of binds.
158 ///
159 /// This implementation prioritizes **stack safety** and **O(1) bind** over HKT trait compatibility.
160 ///
161 /// ### Type Parameters
162 ///
163 #[document_type_parameters(
164 "The base functor (must implement [`Functor`]).",
165 "The result type."
166 )]
167 ///
168 /// ### Examples
169 ///
170 /// ```
171 /// use fp_library::{brands::*, types::*};
172 ///
173 /// let free = Free::<ThunkBrand, _>::pure(42);
174 /// ```
175 pub struct Free<F, A>(pub(crate) Option<FreeInner<F, A>>)
176 where
177 F: Functor + 'static,
178 A: 'static;
179
180 #[document_type_parameters("The base functor.", "The result type.")]
181 #[document_parameters("The Free monad instance to operate on.")]
182 impl<F, A> Free<F, A>
183 where
184 F: Functor + 'static,
185 A: 'static,
186 {
187 /// Creates a pure `Free` value.
188 ///
189 /// ### Type Signature
190 ///
191 #[document_signature]
192 ///
193 /// ### Parameters
194 ///
195 #[document_parameters("The value to wrap.")]
196 ///
197 /// ### Returns
198 ///
199 /// A `Free` computation that produces `a`.
200 ///
201 /// ### Examples
202 ///
203 /// ```
204 /// use fp_library::{brands::*, types::*};
205 ///
206 /// let free = Free::<ThunkBrand, _>::pure(42);
207 /// ```
208 #[inline]
209 pub fn pure(a: A) -> Self {
210 Free(Some(FreeInner::Pure(a)))
211 }
212
213 /// Creates a suspended computation from a functor value.
214 ///
215 /// ### Type Signature
216 ///
217 #[document_signature]
218 ///
219 /// ### Parameters
220 ///
221 #[document_parameters("The functor value containing the next step.")]
222 ///
223 /// ### Returns
224 ///
225 /// A `Free` computation that performs the effect `fa`.
226 ///
227 /// ### Examples
228 ///
229 /// ```
230 /// use fp_library::{brands::*, types::*};
231 ///
232 /// let eval = Thunk::new(|| Free::pure(42));
233 /// let free = Free::<ThunkBrand, _>::wrap(eval);
234 /// ```
235 pub fn wrap(
236 fa: Apply!(<F as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'static, Free<F, A>>)
237 ) -> Self {
238 Free(Some(FreeInner::Wrap(fa)))
239 }
240
241 /// Lifts a functor value into the Free monad.
242 ///
243 /// This is the primary way to inject effects into Free monad computations.
244 /// Equivalent to PureScript's `liftF` and Haskell's `liftF`.
245 ///
246 /// ### Type Signature
247 ///
248 #[document_signature]
249 ///
250 /// ### Implementation
251 ///
252 /// ```text
253 /// liftF fa = wrap (map pure fa)
254 /// ```
255 ///
256 /// ### Parameters
257 ///
258 #[document_parameters("The functor value to lift.")]
259 ///
260 /// ### Returns
261 ///
262 /// A `Free` computation that performs the effect and returns the result.
263 ///
264 /// ### Examples
265 ///
266 /// ```
267 /// use fp_library::{brands::*, types::*};
268 ///
269 /// // Lift a simple computation
270 /// let thunk = Thunk::new(|| 42);
271 /// let free = Free::<ThunkBrand, _>::lift_f(thunk);
272 /// assert_eq!(free.evaluate(), 42);
273 ///
274 /// // Build a computation from raw effects
275 /// let computation = Free::<ThunkBrand, _>::lift_f(Thunk::new(|| 10))
276 /// .bind(|x| Free::lift_f(Thunk::new(move || x * 2)))
277 /// .bind(|x| Free::lift_f(Thunk::new(move || x + 5)));
278 /// assert_eq!(computation.evaluate(), 25);
279 /// ```
280 pub fn lift_f(fa: Apply!(<F as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'static, A>)) -> Self {
281 // Map the value to a pure Free, then wrap it
282 Free::wrap(F::map(Free::pure, fa))
283 }
284
285 /// Monadic bind with O(1) complexity.
286 ///
287 /// ### Type Signature
288 ///
289 #[document_signature]
290 ///
291 /// ### Type Parameters
292 ///
293 #[document_type_parameters("The result type of the new computation.")]
294 ///
295 /// ### Parameters
296 ///
297 #[document_parameters("The function to apply to the result of this computation.")]
298 ///
299 /// ### Returns
300 ///
301 /// A new `Free` computation that chains `f` after this computation.
302 ///
303 /// ### Examples
304 ///
305 /// ```
306 /// use fp_library::{brands::*, types::*};
307 ///
308 /// let free = Free::<ThunkBrand, _>::pure(42)
309 /// .bind(|x| Free::pure(x + 1));
310 /// ```
311 pub fn bind<B: 'static>(
312 mut self,
313 f: impl FnOnce(A) -> Free<F, B> + 'static,
314 ) -> Free<F, B> {
315 // Type-erase the continuation
316 let erased_f: Continuation<F> = Box::new(move |val: TypeErasedValue| {
317 let a: A = *val.downcast().expect("Type mismatch in Free::bind");
318 let free_b: Free<F, B> = f(a);
319 free_b.erase_type()
320 });
321
322 // Extract inner safely
323 let inner = self.0.take().expect("Free value already consumed");
324
325 match inner {
326 // Pure: create a Bind with this continuation
327 FreeInner::Pure(a) => {
328 let head: Free<F, TypeErasedValue> = Free::pure(a).erase_type();
329 Free(Some(FreeInner::Bind {
330 head: Box::new(head),
331 continuations: CatList::singleton(erased_f),
332 _marker: PhantomData,
333 }))
334 }
335
336 // Wrap: wrap in a Bind
337 FreeInner::Wrap(fa) => {
338 let head = Free::wrap(fa).boxed_erase_type();
339 Free(Some(FreeInner::Bind {
340 head,
341 continuations: CatList::singleton(erased_f),
342 _marker: PhantomData,
343 }))
344 }
345
346 // Bind: snoc the new continuation onto the CatList (O(1)!)
347 FreeInner::Bind { head, continuations: conts, .. } => Free(Some(FreeInner::Bind {
348 head,
349 continuations: conts.snoc(erased_f),
350 _marker: PhantomData,
351 })),
352 }
353 }
354
355 /// Converts to type-erased form.
356 ///
357 /// ### Type Signature
358 ///
359 #[document_signature]
360 pub fn erase_type(mut self) -> Free<F, TypeErasedValue> {
361 let inner = self.0.take().expect("Free value already consumed");
362
363 match inner {
364 FreeInner::Pure(a) => Free(Some(FreeInner::Pure(Box::new(a) as TypeErasedValue))),
365 FreeInner::Wrap(fa) => {
366 // Map over the functor to erase the inner type
367 let erased = F::map(|inner: Free<F, A>| inner.erase_type(), fa);
368 Free(Some(FreeInner::Wrap(erased)))
369 }
370 FreeInner::Bind { head, continuations, .. } => {
371 Free(Some(FreeInner::Bind { head, continuations, _marker: PhantomData }))
372 }
373 }
374 }
375
376 /// Converts to boxed type-erased form.
377 ///
378 /// ### Type Signature
379 ///
380 #[document_signature]
381 pub fn boxed_erase_type(self) -> Box<Free<F, TypeErasedValue>> {
382 Box::new(self.erase_type())
383 }
384
385 /// Executes the Free computation, returning the final result.
386 ///
387 /// This is the "trampoline" that iteratively processes the
388 /// [`CatList`] of continuations without growing the stack.
389 ///
390 /// ### Type Signature
391 ///
392 #[document_signature]
393 ///
394 /// ### Returns
395 ///
396 /// The final result of the computation.
397 ///
398 /// ### Examples
399 ///
400 /// ```
401 /// use fp_library::{brands::*, types::*};
402 ///
403 /// let free = Free::<ThunkBrand, _>::pure(42);
404 /// assert_eq!(free.evaluate(), 42);
405 /// ```
406 pub fn evaluate(self) -> A
407 where
408 F: Evaluable,
409 {
410 // Start with a type-erased version
411 let mut current: Free<F, TypeErasedValue> = self.erase_type();
412 let mut continuations: CatList<Continuation<F>> = CatList::empty();
413
414 loop {
415 let inner = current.0.take().expect("Free value already consumed");
416
417 match inner {
418 FreeInner::Pure(val) => {
419 // Try to apply the next continuation
420 match continuations.uncons() {
421 Some((continuation, rest)) => {
422 current = continuation(val);
423 continuations = rest;
424 }
425 None => {
426 // No more continuations - we're done!
427 return *val
428 .downcast::<A>()
429 .expect("Type mismatch in Free::evaluate final downcast");
430 }
431 }
432 }
433
434 FreeInner::Wrap(fa) => {
435 // Run the effect to get the inner Free
436 current = <F as Evaluable>::evaluate(fa);
437 }
438
439 FreeInner::Bind { head, continuations: inner_continuations, .. } => {
440 // Merge the inner continuations with outer ones
441 // This is where CatList's O(1) append shines!
442 current = *head;
443 continuations = inner_continuations.append(continuations);
444 }
445 }
446 }
447 }
448 }
449
450 /// ### Type Parameters
451 ///
452 #[document_type_parameters("The base functor.", "The result type.")]
453 #[document_parameters("The free monad instance to drop.")]
454 impl<F, A> Drop for Free<F, A>
455 where
456 F: Functor + 'static,
457 A: 'static,
458 {
459 /// ### Type Signature
460 ///
461 #[document_signature]
462 fn drop(&mut self) {
463 // We take the inner value out.
464 let inner = self.0.take();
465
466 // If the top level is a Bind, we need to start the iterative drop chain.
467 if let Some(FreeInner::Bind { mut head, .. }) = inner {
468 // head is Box<Free<F, TypeEraseValue>>.
469 // We take its inner value to continue the chain.
470 // From now on, everything is typed as FreeInner<F, TypeEraseValue>.
471 let mut current = head.0.take();
472
473 while let Some(FreeInner::Bind { mut head, .. }) = current {
474 current = head.0.take();
475 }
476 }
477 }
478 }
479
480 #[document_type_parameters("The result type.")]
481 impl<A: 'static> Deferrable<'static> for Free<ThunkBrand, A> {
482 /// Creates a `Free` computation from a thunk.
483 ///
484 /// This delegates to `Free::wrap` and `Thunk::new`.
485 ///
486 /// ### Type Signature
487 ///
488 #[document_signature]
489 ///
490 /// ### Type Parameters
491 ///
492 #[document_type_parameters("The type of the thunk.")]
493 ///
494 /// ### Parameters
495 ///
496 #[document_parameters("A thunk that produces the free computation.")]
497 ///
498 /// ### Returns
499 ///
500 /// The deferred free computation.
501 ///
502 /// ### Examples
503 ///
504 /// ```
505 /// use fp_library::{brands::*, functions::*, types::*, classes::Deferrable};
506 ///
507 /// let task: Free<ThunkBrand, i32> = Deferrable::defer(|| Free::pure(42));
508 /// assert_eq!(task.evaluate(), 42);
509 /// ```
510 fn defer<F>(f: F) -> Self
511 where
512 F: FnOnce() -> Self + 'static,
513 Self: Sized,
514 {
515 Self::wrap(Thunk::new(f))
516 }
517 }
518}
519pub use inner::*;
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524 use crate::{brands::ThunkBrand, types::thunk::Thunk};
525
526 /// Tests `Free::pure`.
527 ///
528 /// **What it tests:** Verifies that `pure` creates a computation that simply returns the provided value.
529 /// **How it tests:** Constructs a `Free::pure(42)` and runs it, asserting the result is 42.
530 #[test]
531 fn test_free_pure() {
532 let free = Free::<ThunkBrand, _>::pure(42);
533 assert_eq!(free.evaluate(), 42);
534 }
535
536 /// Tests `Free::roll`.
537 ///
538 /// **What it tests:** Verifies that `roll` creates a computation from a suspended effect.
539 /// **How it tests:** Wraps a `Free::pure(42)` inside a `Thunk`, rolls it into a `Free`, and runs it to ensure it unwraps correctly.
540 #[test]
541 fn test_free_roll() {
542 let eval = Thunk::new(|| Free::pure(42));
543 let free = Free::<ThunkBrand, _>::wrap(eval);
544 assert_eq!(free.evaluate(), 42);
545 }
546
547 /// Tests `Free::bind`.
548 ///
549 /// **What it tests:** Verifies that `bind` correctly chains computations and passes values between them.
550 /// **How it tests:** Chains `pure(42) -> bind(+1) -> bind(*2)` and asserts the result is (42+1)*2 = 86.
551 #[test]
552 fn test_free_bind() {
553 let free =
554 Free::<ThunkBrand, _>::pure(42).bind(|x| Free::pure(x + 1)).bind(|x| Free::pure(x * 2));
555 assert_eq!(free.evaluate(), 86);
556 }
557
558 /// Tests stack safety of `Free::evaluate`.
559 ///
560 /// **What it tests:** Verifies that `run` can handle deep recursion without stack overflow (trampolining).
561 /// **How it tests:** Creates a recursive `count_down` function that builds a chain of 100,000 `bind` calls.
562 /// If the implementation were not stack-safe, this would crash with a stack overflow.
563 #[test]
564 fn test_free_stack_safety() {
565 fn count_down(n: i32) -> Free<ThunkBrand, i32> {
566 if n == 0 { Free::pure(0) } else { Free::pure(n).bind(|n| count_down(n - 1)) }
567 }
568
569 // 100,000 iterations should overflow stack if not safe
570 let free = count_down(100_000);
571 assert_eq!(free.evaluate(), 0);
572 }
573
574 /// Tests stack safety of `Free::drop`.
575 ///
576 /// **What it tests:** Verifies that dropping a deep `Free` computation does not cause a stack overflow.
577 /// **How it tests:** Constructs a deep `Free` chain (similar to `test_free_stack_safety`) and lets it go out of scope.
578 #[test]
579 fn test_free_drop_safety() {
580 fn count_down(n: i32) -> Free<ThunkBrand, i32> {
581 if n == 0 { Free::pure(0) } else { Free::pure(n).bind(|n| count_down(n - 1)) }
582 }
583
584 // Construct a deep chain but DO NOT run it.
585 // When `free` goes out of scope, `Drop` should handle it iteratively.
586 let _free = count_down(100_000);
587 }
588
589 /// Tests `Free::bind` on a `Wrap` variant.
590 ///
591 /// **What it tests:** Verifies that `bind` works correctly when applied to a suspended computation (`Wrap`).
592 /// **How it tests:** Creates a `Wrap` (via `wrap`) and `bind`s it.
593 #[test]
594 fn test_free_bind_on_wrap() {
595 let eval = Thunk::new(|| Free::pure(42));
596 let free = Free::<ThunkBrand, _>::wrap(eval).bind(|x| Free::pure(x + 1));
597 assert_eq!(free.evaluate(), 43);
598 }
599
600 /// Tests `Free::lift_f`.
601 ///
602 /// **What it tests:** Verifies that `lift_f` correctly lifts a functor value into the Free monad.
603 /// **How it tests:** Lifts a simple thunk and verifies the result.
604 #[test]
605 fn test_free_lift_f() {
606 let thunk = Thunk::new(|| 42);
607 let free = Free::<ThunkBrand, _>::lift_f(thunk);
608 assert_eq!(free.evaluate(), 42);
609 }
610
611 /// Tests `Free::lift_f` with bind.
612 ///
613 /// **What it tests:** Verifies that `lift_f` can be used to build computations with `bind`.
614 /// **How it tests:** Chains multiple `lift_f` calls with `bind`.
615 #[test]
616 fn test_free_lift_f_with_bind() {
617 let computation = Free::<ThunkBrand, _>::lift_f(Thunk::new(|| 10))
618 .bind(|x| Free::<ThunkBrand, _>::lift_f(Thunk::new(move || x * 2)))
619 .bind(|x| Free::<ThunkBrand, _>::lift_f(Thunk::new(move || x + 5)));
620 assert_eq!(computation.evaluate(), 25);
621 }
622}