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