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	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}