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