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