Skip to main content

fp_library/types/
trampoline.rs

1//! Stack-safe computation type with guaranteed safety for unlimited recursion depth.
2//!
3//! Built on the [`Free`](crate::types::Free) monad with O(1) [`bind`](crate::functions::bind) operations. Provides complete stack safety at the cost of requiring `'static` types. Use this for deep recursion and heavy monadic pipelines.
4//!
5//! ### Examples
6//!
7//! ```
8//! use fp_library::types::*;
9//!
10//! let task = Trampoline::new(|| 1 + 1)
11//! 	.bind(|x| Trampoline::new(move || x * 2))
12//! 	.bind(|x| Trampoline::new(move || x + 10));
13//!
14//! assert_eq!(task.evaluate(), 14);
15//! ```
16
17#[fp_macros::document_module]
18mod inner {
19	use {
20		crate::{
21			brands::ThunkBrand,
22			classes::Deferrable,
23			types::{
24				Free,
25				Lazy,
26				LazyConfig,
27				Step,
28				Thunk,
29			},
30		},
31		fp_macros::*,
32	};
33
34	/// A lazy, stack-safe computation that produces a value of type `A`.
35	///
36	/// `Trampoline` is the "heavy-duty" monadic type for deferred computations that
37	/// require **guaranteed stack safety**. It is built on [`Free<Thunk, A>`] with
38	/// [`CatList`](crate::types::CatList)-based bind stack, ensuring O(1) [`bind`](crate::functions::bind)
39	/// operations and unlimited recursion depth without stack overflow.
40	///
41	/// # Requirements
42	///
43	/// - `A: 'static + Send` - Required due to type erasure via [`Box<dyn Any>`].
44	///
45	/// # Guarantees
46	///
47	/// - **Stack safe**: Will not overflow regardless of recursion depth.
48	/// - **O(1) bind**: Left-associated `bind` chains don't degrade.
49	/// - **Lazy**: Computation is deferred until [`Trampoline::evaluate`] is called.
50	///
51	/// # When to Use `Trampoline` vs [`Thunk`]
52	///
53	/// - Use **`Trampoline<A>`** for deep recursion, heavy monadic pipelines.
54	/// - Use **`Thunk<'a, A>`** for HKT integration, borrowed references, glue code.
55	///
56	/// # Memoization
57	///
58	/// `Trampoline` does NOT memoize. Each call to `run` re-evaluates.
59	/// For memoization, wrap in [`Lazy`]:
60	///
61	/// ```rust
62	/// use fp_library::types::*;
63	///
64	/// let lazy: Lazy<i32> = Lazy::<_, RcLazyConfig>::new(|| Trampoline::new(|| 1 + 1).evaluate());
65	/// lazy.evaluate(); // Computes
66	/// lazy.evaluate(); // Returns cached
67	/// ```
68	#[document_type_parameters("The type of the value produced by the task.")]
69	///
70	pub struct Trampoline<A: 'static>(
71		/// The internal `Free` monad representation.
72		Free<ThunkBrand, A>,
73	);
74
75	#[document_type_parameters("The type of the value produced by the task.")]
76	#[document_parameters("The `Trampoline` instance.")]
77	impl<A: 'static + Send> Trampoline<A> {
78		/// Creates a `Trampoline` from an already-computed value.
79		///
80		/// ### Complexity
81		///
82		/// O(1) creation, O(1) evaluation
83		#[document_signature]
84		///
85		#[document_parameters("The value to wrap.")]
86		///
87		#[document_returns("A `Trampoline` that produces the value `a`.")]
88		///
89		#[inline]
90		#[document_examples]
91		///
92		/// ```
93		/// use fp_library::types::*;
94		///
95		/// let task = Trampoline::pure(42);
96		/// assert_eq!(task.evaluate(), 42);
97		/// ```
98		pub fn pure(a: A) -> Self {
99			Trampoline(Free::pure(a))
100		}
101
102		/// Creates a lazy `Trampoline` that computes `f` on first evaluation.
103		///
104		/// `Trampoline` does NOT memoize - each `evaluate()`
105		/// re-evaluates. Use [`Lazy`] for caching.
106		///
107		/// # Complexity
108		/// O(1) creation
109		#[document_signature]
110		///
111		#[document_parameters("The closure to execute.")]
112		///
113		#[document_returns("A `Trampoline` that executes `f` when run.")]
114		///
115		#[inline]
116		#[document_examples]
117		///
118		/// ```
119		/// use fp_library::types::*;
120		///
121		/// let task = Trampoline::new(|| {
122		/// 	// println!("Computing!");
123		/// 	1 + 1
124		/// });
125		///
126		/// // Nothing printed yet
127		/// let result = task.evaluate(); // Prints "Computing!"
128		/// assert_eq!(result, 2);
129		/// ```
130		pub fn new(f: impl FnOnce() -> A + 'static) -> Self {
131			Trampoline(Free::wrap(Thunk::new(move || Free::pure(f()))))
132		}
133
134		/// Defers the construction of a `Trampoline` itself.
135		///
136		/// This is critical for stack-safe recursion: instead of
137		/// building a chain of `Trampoline`s directly (which grows the stack),
138		/// we defer the construction.
139		#[document_signature]
140		///
141		#[document_parameters("The closure that produces a `Trampoline`.")]
142		///
143		#[document_returns("A `Trampoline` that defers the creation of the inner task.")]
144		///
145		#[inline]
146		#[document_examples]
147		///
148		/// ```
149		/// use fp_library::types::*;
150		///
151		/// fn recursive_sum(
152		/// 	n: u64,
153		/// 	acc: u64,
154		/// ) -> Trampoline<u64> {
155		/// 	if n == 0 {
156		/// 		Trampoline::pure(acc)
157		/// 	} else {
158		/// 		// Defer construction to avoid stack growth
159		/// 		Trampoline::defer(move || recursive_sum(n - 1, acc + n))
160		/// 	}
161		/// }
162		///
163		/// // This works for n = 1_000_000 without stack overflow!
164		/// let result = recursive_sum(1_000, 0).evaluate();
165		/// assert_eq!(result, 500500);
166		/// ```
167		pub fn defer(f: impl FnOnce() -> Trampoline<A> + 'static) -> Self {
168			Trampoline(Free::wrap(Thunk::new(move || f().0)))
169		}
170
171		/// Monadic bind with O(1) complexity.
172		///
173		/// Chains computations together. The key property is that
174		/// left-associated chains don't degrade to O(n²).
175		#[document_signature]
176		///
177		#[document_type_parameters("The type of the result of the new task.")]
178		///
179		#[document_parameters("The function to apply to the result of this task.")]
180		///
181		#[document_returns("A new `Trampoline` that chains `f` after this task.")]
182		///
183		#[inline]
184		#[document_examples]
185		///
186		/// ```
187		/// use fp_library::types::*;
188		///
189		/// // This is O(n), not O(n²)
190		/// let mut task = Trampoline::pure(0);
191		/// for i in 0 .. 100 {
192		/// 	task = task.bind(move |x| Trampoline::pure(x + i));
193		/// }
194		/// assert_eq!(task.evaluate(), 4950);
195		/// ```
196		pub fn bind<B: 'static + Send>(
197			self,
198			f: impl FnOnce(A) -> Trampoline<B> + 'static,
199		) -> Trampoline<B> {
200			Trampoline(self.0.bind(move |a| f(a).0))
201		}
202
203		/// Functor map: transforms the result without changing structure.
204		#[document_signature]
205		///
206		#[document_type_parameters("The type of the result of the mapping function.")]
207		///
208		#[document_parameters("The function to apply to the result of this task.")]
209		///
210		#[document_returns("A new `Trampoline` with the transformed result.")]
211		///
212		#[inline]
213		#[document_examples]
214		///
215		/// ```
216		/// use fp_library::types::*;
217		///
218		/// let task = Trampoline::pure(10).map(|x| x * 2);
219		/// assert_eq!(task.evaluate(), 20);
220		/// ```
221		pub fn map<B: 'static + Send>(
222			self,
223			f: impl FnOnce(A) -> B + 'static,
224		) -> Trampoline<B> {
225			self.bind(move |a| Trampoline::pure(f(a)))
226		}
227
228		/// Forces evaluation and returns the result.
229		///
230		/// This runs the trampoline loop, iteratively processing
231		/// the CatList of continuations without growing the stack.
232		#[document_signature]
233		///
234		#[document_parameters]
235		///
236		#[document_returns("The result of the computation.")]
237		///
238		#[document_examples]
239		///
240		/// ```
241		/// use fp_library::types::*;
242		///
243		/// let task = Trampoline::new(|| 1 + 1);
244		/// assert_eq!(task.evaluate(), 2);
245		/// ```
246		pub fn evaluate(self) -> A {
247			self.0.evaluate()
248		}
249
250		/// Combines two `Trampoline`s, running both and combining results.
251		#[document_signature]
252		///
253		#[document_type_parameters(
254			"The type of the second task's result.",
255			"The type of the combined result."
256		)]
257		///
258		#[document_parameters("The second task.", "The function to combine the results.")]
259		///
260		#[document_returns("A new `Trampoline` producing the combined result.")]
261		///
262		#[document_examples]
263		///
264		/// ```
265		/// use fp_library::types::*;
266		///
267		/// let t1 = Trampoline::pure(10);
268		/// let t2 = Trampoline::pure(20);
269		/// let t3 = t1.lift2(t2, |a, b| a + b);
270		/// assert_eq!(t3.evaluate(), 30);
271		/// ```
272		pub fn lift2<B: 'static + Send, C: 'static + Send>(
273			self,
274			other: Trampoline<B>,
275			f: impl FnOnce(A, B) -> C + 'static,
276		) -> Trampoline<C> {
277			self.bind(move |a| other.map(move |b| f(a, b)))
278		}
279
280		/// Sequences two `Trampoline`s, discarding the first result.
281		#[document_signature]
282		///
283		#[document_type_parameters("The type of the second task's result.")]
284		///
285		#[document_parameters("The second task.")]
286		///
287		#[document_returns(
288			"A new `Trampoline` that runs both tasks and returns the result of the second."
289		)]
290		///
291		#[document_examples]
292		///
293		/// ```
294		/// use fp_library::types::*;
295		///
296		/// let t1 = Trampoline::pure(10);
297		/// let t2 = Trampoline::pure(20);
298		/// let t3 = t1.then(t2);
299		/// assert_eq!(t3.evaluate(), 20);
300		/// ```
301		pub fn then<B: 'static + Send>(
302			self,
303			other: Trampoline<B>,
304		) -> Trampoline<B> {
305			self.bind(move |_| other)
306		}
307
308		/// Stack-safe tail recursion within Trampoline.
309		///
310		/// # Clone Bound
311		///
312		/// The function `f` must implement `Clone` because each iteration
313		/// of the recursion may need its own copy. Most closures naturally
314		/// implement `Clone` when all their captures implement `Clone`.
315		///
316		/// For closures that don't implement `Clone`, use `arc_tail_rec_m`
317		/// which wraps the closure in `Arc` internally.
318		#[document_signature]
319		///
320		#[document_type_parameters("The type of the state.")]
321		///
322		#[document_parameters(
323			"The function that performs one step of the recursion.",
324			"The initial state."
325		)]
326		///
327		#[document_returns("A `Trampoline` that performs the recursion.")]
328		#[document_examples]
329		///
330		/// ```
331		/// use fp_library::types::{
332		/// 	Step,
333		/// 	Trampoline,
334		/// };
335		///
336		/// // Fibonacci using tail recursion
337		/// fn fib(n: u64) -> Trampoline<u64> {
338		/// 	Trampoline::tail_rec_m(
339		/// 		|(n, a, b)| {
340		/// 			if n == 0 {
341		/// 				Trampoline::pure(Step::Done(a))
342		/// 			} else {
343		/// 				Trampoline::pure(Step::Loop((n - 1, b, a + b)))
344		/// 			}
345		/// 		},
346		/// 		(n, 0u64, 1u64),
347		/// 	)
348		/// }
349		///
350		/// assert_eq!(fib(50).evaluate(), 12586269025);
351		/// ```
352		pub fn tail_rec_m<S: 'static + Send>(
353			f: impl Fn(S) -> Trampoline<Step<S, A>> + Clone + 'static,
354			initial: S,
355		) -> Self {
356			// Use defer to ensure each step is trampolined.
357			fn go<A: 'static + Send, B: 'static + Send, F>(
358				f: F,
359				a: A,
360			) -> Trampoline<B>
361			where
362				F: Fn(A) -> Trampoline<Step<A, B>> + Clone + 'static, {
363				let f_clone = f.clone();
364				Trampoline::defer(move || {
365					f(a).bind(move |step| match step {
366						Step::Loop(next) => go(f_clone.clone(), next),
367						Step::Done(b) => Trampoline::pure(b),
368					})
369				})
370			}
371
372			go(f, initial)
373		}
374
375		/// Arc-wrapped version for non-Clone closures.
376		///
377		/// Use this when your closure captures non-Clone state.
378		#[document_signature]
379		///
380		#[document_type_parameters("The type of the state.")]
381		///
382		#[document_parameters(
383			"The function that performs one step of the recursion.",
384			"The initial state."
385		)]
386		///
387		#[document_returns("A `Trampoline` that performs the recursion.")]
388		#[document_examples]
389		///
390		/// ```
391		/// use {
392		/// 	fp_library::types::{
393		/// 		Step,
394		/// 		Trampoline,
395		/// 	},
396		/// 	std::sync::{
397		/// 		Arc,
398		/// 		atomic::{
399		/// 			AtomicUsize,
400		/// 			Ordering,
401		/// 		},
402		/// 	},
403		/// };
404		///
405		/// // Closure captures non-Clone state
406		/// let counter = Arc::new(AtomicUsize::new(0));
407		/// let counter_clone = Arc::clone(&counter);
408		/// let task = Trampoline::arc_tail_rec_m(
409		/// 	move |n| {
410		/// 		counter_clone.fetch_add(1, Ordering::SeqCst);
411		/// 		if n == 0 {
412		/// 			Trampoline::pure(Step::Done(0))
413		/// 		} else {
414		/// 			Trampoline::pure(Step::Loop(n - 1))
415		/// 		}
416		/// 	},
417		/// 	100,
418		/// );
419		/// assert_eq!(task.evaluate(), 0);
420		/// assert_eq!(counter.load(Ordering::SeqCst), 101);
421		/// ```
422		pub fn arc_tail_rec_m<S: 'static + Send>(
423			f: impl Fn(S) -> Trampoline<Step<S, A>> + 'static,
424			initial: S,
425		) -> Self {
426			use std::sync::Arc;
427			let f = Arc::new(f);
428			let wrapper = move |s: S| {
429				let f = Arc::clone(&f);
430				f(s)
431			};
432			Self::tail_rec_m(wrapper, initial)
433		}
434	}
435
436	#[document_type_parameters(
437		"The type of the value produced by the task.",
438		"The memoization configuration."
439	)]
440	impl<A: 'static + Send + Clone, Config: LazyConfig> From<Lazy<'static, A, Config>>
441		for Trampoline<A>
442	{
443		#[document_signature]
444		#[document_parameters("The lazy value to convert.")]
445		#[document_returns("A trampoline that evaluates the lazy value.")]
446		#[document_examples]
447		///
448		/// ```
449		/// use fp_library::types::*;
450		/// let lazy = Lazy::<_, RcLazyConfig>::pure(42);
451		/// let task = Trampoline::from(lazy);
452		/// assert_eq!(task.evaluate(), 42);
453		/// ```
454		fn from(lazy: Lazy<'static, A, Config>) -> Self {
455			Trampoline::new(move || lazy.evaluate().clone())
456		}
457	}
458
459	#[document_type_parameters("The type of the value produced by the task.")]
460	impl<A: 'static + Send> Deferrable<'static> for Trampoline<A> {
461		/// Creates a `Trampoline` from a computation that produces it.
462		#[document_signature]
463		///
464		#[document_parameters("A thunk that produces the trampoline.")]
465		///
466		#[document_returns("The deferred trampoline.")]
467		///
468		#[document_examples]
469		///
470		/// ```
471		/// use fp_library::{
472		/// 	brands::*,
473		/// 	classes::Deferrable,
474		/// 	functions::*,
475		/// 	types::*,
476		/// };
477		///
478		/// let task: Trampoline<i32> = Deferrable::defer(|| Trampoline::pure(42));
479		/// assert_eq!(task.evaluate(), 42);
480		/// ```
481		fn defer(f: impl FnOnce() -> Self + 'static) -> Self
482		where
483			Self: Sized, {
484			Trampoline::defer(f)
485		}
486	}
487}
488pub use inner::*;
489
490#[cfg(test)]
491mod tests {
492	use {
493		super::*,
494		crate::types::step::Step,
495	};
496
497	/// Tests `Trampoline::pure`.
498	///
499	/// Verifies that `pure` creates a task that returns the value immediately.
500	#[test]
501	fn test_task_pure() {
502		let task = Trampoline::pure(42);
503		assert_eq!(task.evaluate(), 42);
504	}
505
506	/// Tests `Trampoline::new`.
507	///
508	/// Verifies that `new` creates a task that computes the value when run.
509	#[test]
510	fn test_task_new() {
511		let task = Trampoline::new(|| 42);
512		assert_eq!(task.evaluate(), 42);
513	}
514
515	/// Tests `Trampoline::bind`.
516	///
517	/// Verifies that `bind` chains computations correctly.
518	#[test]
519	fn test_task_bind() {
520		let task = Trampoline::pure(10).bind(|x| Trampoline::pure(x * 2));
521		assert_eq!(task.evaluate(), 20);
522	}
523
524	/// Tests `Trampoline::map`.
525	///
526	/// Verifies that `map` transforms the result.
527	#[test]
528	fn test_task_map() {
529		let task = Trampoline::pure(10).map(|x| x * 2);
530		assert_eq!(task.evaluate(), 20);
531	}
532
533	/// Tests `Trampoline::defer`.
534	///
535	/// Verifies that `defer` delays the creation of the task.
536	#[test]
537	fn test_task_defer() {
538		let task = Trampoline::defer(|| Trampoline::pure(42));
539		assert_eq!(task.evaluate(), 42);
540	}
541
542	/// Tests `Trampoline::tail_rec_m`.
543	///
544	/// Verifies that `tail_rec_m` performs tail recursion correctly.
545	#[test]
546	fn test_task_tail_rec_m() {
547		fn factorial(n: u64) -> Trampoline<u64> {
548			Trampoline::tail_rec_m(
549				|(n, acc)| {
550					if n <= 1 {
551						Trampoline::pure(Step::Done(acc))
552					} else {
553						Trampoline::pure(Step::Loop((n - 1, n * acc)))
554					}
555				},
556				(n, 1u64),
557			)
558		}
559
560		assert_eq!(factorial(5).evaluate(), 120);
561	}
562
563	/// Tests `Trampoline::map2`.
564	///
565	/// Verifies that `map2` combines two tasks.
566	#[test]
567	fn test_task_map2() {
568		let t1 = Trampoline::pure(10);
569		let t2 = Trampoline::pure(20);
570		let t3 = t1.lift2(t2, |a, b| a + b);
571		assert_eq!(t3.evaluate(), 30);
572	}
573
574	/// Tests `Trampoline::and_then`.
575	///
576	/// Verifies that `and_then` sequences two tasks.
577	#[test]
578	fn test_task_and_then() {
579		let t1 = Trampoline::pure(10);
580		let t2 = Trampoline::pure(20);
581		let t3 = t1.then(t2);
582		assert_eq!(t3.evaluate(), 20);
583	}
584
585	/// Tests `Trampoline::arc_tail_rec_m`.
586	///
587	/// Verifies that `arc_tail_rec_m` works with non-Clone closures.
588	#[test]
589	fn test_task_arc_tail_rec_m() {
590		use std::sync::{
591			Arc,
592			atomic::{
593				AtomicUsize,
594				Ordering,
595			},
596		};
597
598		let counter = Arc::new(AtomicUsize::new(0));
599		let counter_clone = Arc::clone(&counter);
600
601		let task = Trampoline::arc_tail_rec_m(
602			move |n| {
603				counter_clone.fetch_add(1, Ordering::SeqCst);
604				if n == 0 {
605					Trampoline::pure(Step::Done(0))
606				} else {
607					Trampoline::pure(Step::Loop(n - 1))
608				}
609			},
610			10,
611		);
612
613		assert_eq!(task.evaluate(), 0);
614		assert_eq!(counter.load(Ordering::SeqCst), 11);
615	}
616
617	/// Tests `Trampoline::from_memo`.
618	///
619	/// Verifies that `From<Lazy>` creates a task that retrieves the memoized value lazily.
620	#[test]
621	fn test_task_from_memo() {
622		use {
623			crate::types::{
624				Lazy,
625				RcLazyConfig,
626			},
627			std::{
628				cell::RefCell,
629				rc::Rc,
630			},
631		};
632
633		let counter = Rc::new(RefCell::new(0));
634		let counter_clone = counter.clone();
635		let memo = Lazy::<_, RcLazyConfig>::new(move || {
636			*counter_clone.borrow_mut() += 1;
637			42
638		});
639
640		let task = Trampoline::from(memo.clone());
641
642		// Should not have computed yet (lazy creation)
643		assert_eq!(*counter.borrow(), 0);
644
645		assert_eq!(task.evaluate(), 42);
646		assert_eq!(*counter.borrow(), 1);
647
648		// Run again, should use cached value
649		let task2 = Trampoline::from(memo);
650		assert_eq!(task2.evaluate(), 42);
651		assert_eq!(*counter.borrow(), 1);
652	}
653
654	/// Tests `Trampoline::from` with `ArcLazy`.
655	#[test]
656	fn test_task_from_arc_memo() {
657		use {
658			crate::types::{
659				ArcLazyConfig,
660				Lazy,
661			},
662			std::sync::{
663				Arc,
664				Mutex,
665			},
666		};
667
668		let counter = Arc::new(Mutex::new(0));
669		let counter_clone = counter.clone();
670		let memo = Lazy::<_, ArcLazyConfig>::new(move || {
671			*counter_clone.lock().unwrap() += 1;
672			42
673		});
674
675		let task = Trampoline::from(memo.clone());
676
677		// Should not have computed yet (lazy creation)
678		assert_eq!(*counter.lock().unwrap(), 0);
679
680		assert_eq!(task.evaluate(), 42);
681		assert_eq!(*counter.lock().unwrap(), 1);
682
683		// Run again, should use cached value
684		let task2 = Trampoline::from(memo);
685		assert_eq!(task2.evaluate(), 42);
686		assert_eq!(*counter.lock().unwrap(), 1);
687	}
688}