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