Skip to main content

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}