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