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