Skip to main content

fp_library/types/
send_thunk.rs

1//! Thread-safe deferred, non-memoized computation.
2//!
3//! Like [`Thunk`](crate::types::Thunk) but with a `Send` bound on the inner closure,
4//! enabling thread-safe deferred computation chains and truly lazy
5//! [`into_arc_lazy`](SendThunk::into_arc_lazy) without eager evaluation.
6//!
7//! Standard HKT traits (`Functor`, `Semimonad`, etc.) cannot be implemented because
8//! their signatures do not require `Send` on mapping functions. Use the inherent
9//! methods ([`map`](SendThunk::map), [`bind`](SendThunk::bind)) instead. The corresponding brand is [`SendThunkBrand`](crate::brands::SendThunkBrand).
10
11#[fp_macros::document_module]
12mod inner {
13	use {
14		crate::{
15			brands::SendThunkBrand,
16			classes::{
17				CloneFn,
18				Foldable,
19				FoldableWithIndex,
20				LiftFn,
21				Monoid,
22				Semigroup,
23				SendDeferrable,
24				WithIndex,
25			},
26			impl_kind,
27			kinds::*,
28			types::{
29				ArcLazy,
30				Thunk,
31			},
32		},
33		core::ops::ControlFlow,
34		fp_macros::*,
35		std::{
36			fmt,
37			sync::Arc,
38		},
39	};
40
41	/// A thread-safe deferred computation that produces a value of type `A`.
42	///
43	/// `SendThunk` is the `Send`-capable counterpart of [`Thunk`]. It wraps a
44	/// `Box<dyn FnOnce() -> A + Send + 'a>`, so it can be transferred across thread
45	/// boundaries. Like `Thunk`, it is NOT memoized and does not cache results.
46	///
47	/// The key advantage over `Thunk` is that [`into_arc_lazy`](SendThunk::into_arc_lazy)
48	/// can wrap the closure lazily in an [`ArcLazy`] without forcing evaluation
49	/// first, because the inner closure satisfies `Send`.
50	///
51	/// ### Higher-Kinded Type Representation
52	///
53	/// The higher-kinded representation of this type constructor is
54	/// [`SendThunkBrand`](crate::brands::SendThunkBrand), which is fully
55	/// polymorphic over the result type.
56	///
57	/// ### Trade-offs vs Other Lazy Types
58	///
59	/// | Aspect         | `SendThunk<'a, A>`            | `Thunk<'a, A>`                | `Trampoline<A>`              | `ArcLazy<'a, A>`             |
60	/// |----------------|-------------------------------|-------------------------------|------------------------------|------------------------------|
61	/// | Thread safety  | `Send`                        | Not `Send`                    | Not `Send`                   | `Send + Sync`                |
62	/// | HKT compatible | No (needs `Send` closures)    | Yes                           | No (requires `'static`)      | Partial (`SendRefFunctor`)   |
63	/// | Stack-safe     | Partial (`tail_rec_m` only)   | Partial (`tail_rec_m` only)   | Yes (unlimited)              | N/A (memoized)               |
64	/// | Memoized       | No                            | No                            | No                           | Yes                          |
65	/// | Lifetime       | `'a` (can borrow)             | `'a` (can borrow)             | `'static` only               | `'a` (can borrow)            |
66	/// | Use case       | Cross-thread lazy pipelines   | Glue code, composition        | Deep recursion, pipelines    | Shared cached values          |
67	///
68	/// ### HKT Trait Limitations
69	///
70	/// Standard HKT traits such as `Functor`, `Pointed`, `Semimonad`, and
71	/// `Semiapplicative` cannot be implemented for `SendThunkBrand` because
72	/// their signatures do not require `Send` on the mapping or binding
73	/// functions. Since `SendThunk` stores a `Box<dyn FnOnce() -> A + Send>`,
74	/// composing it with a non-`Send` closure would violate the `Send` invariant.
75	///
76	/// Use the inherent methods ([`map`](SendThunk::map),
77	/// [`bind`](SendThunk::bind)) instead, which accept `Send` closures
78	/// explicitly.
79	///
80	/// ### Algebraic Properties
81	///
82	/// `SendThunk` satisfies the monad laws through its inherent methods, even though
83	/// it cannot implement the HKT `Monad` trait (due to the `Send` bound requirement):
84	/// - `pure(a).bind(f) ≡ f(a)` (left identity).
85	/// - `m.bind(|x| pure(x)) ≡ m` (right identity).
86	/// - `m.bind(f).bind(g) ≡ m.bind(|x| f(x).bind(g))` (associativity).
87	///
88	/// ### Stack Safety
89	///
90	/// Like `Thunk`, `SendThunk::bind` chains are **not** stack-safe. Each nested
91	/// [`bind`](SendThunk::bind) adds a frame to the call stack.
92	#[document_type_parameters(
93		"The lifetime of the computation.",
94		"The type of the value produced by the computation."
95	)]
96	///
97	pub struct SendThunk<'a, A>(
98		/// The thread-safe closure that performs the computation.
99		Box<dyn FnOnce() -> A + Send + 'a>,
100	);
101
102	// INVARIANT: SendThunk is Send because its inner closure is Send.
103	// The Box<dyn FnOnce() -> A + Send + 'a> already guarantees Send on the closure.
104	// Rust auto-derives Send for Box<dyn ... + Send>, so this is sound.
105
106	#[document_type_parameters(
107		"The lifetime of the computation.",
108		"The type of the value produced by the computation."
109	)]
110	#[document_parameters("The send thunk instance.")]
111	impl<'a, A: 'a> SendThunk<'a, A> {
112		/// Returns the inner boxed closure, erasing the `Send` bound.
113		///
114		/// This is a crate-internal helper used by `From<SendThunk> for Thunk`
115		/// to perform a zero-cost unsizing coercion.
116		#[document_signature]
117		#[document_returns("The inner boxed closure with the `Send` bound erased.")]
118		#[document_examples]
119		///
120		/// ```
121		/// use fp_library::types::*;
122		///
123		/// let send_thunk = SendThunk::new(|| 42);
124		/// let thunk = Thunk::from(send_thunk);
125		/// assert_eq!(thunk.evaluate(), 42);
126		/// ```
127		#[inline]
128		pub(crate) fn into_inner(self) -> Box<dyn FnOnce() -> A + 'a> {
129			self.0
130		}
131
132		/// Creates a new `SendThunk` from a thread-safe closure.
133		#[document_signature]
134		///
135		#[document_parameters("The thread-safe closure to wrap.")]
136		///
137		#[document_returns("A new `SendThunk` instance.")]
138		///
139		#[document_examples]
140		///
141		/// ```
142		/// use fp_library::types::*;
143		///
144		/// let thunk = SendThunk::new(|| 42);
145		/// assert_eq!(thunk.evaluate(), 42);
146		/// ```
147		#[inline]
148		pub fn new(f: impl FnOnce() -> A + Send + 'a) -> Self {
149			SendThunk(Box::new(f))
150		}
151
152		/// Returns a pure value (already computed).
153		#[document_signature]
154		///
155		#[document_parameters("The value to wrap.")]
156		///
157		#[document_returns("A new `SendThunk` instance containing the value.")]
158		///
159		#[document_examples]
160		///
161		/// ```
162		/// use fp_library::types::*;
163		///
164		/// let thunk = SendThunk::pure(42);
165		/// assert_eq!(thunk.evaluate(), 42);
166		/// ```
167		#[inline]
168		pub fn pure(a: A) -> Self
169		where
170			A: Send + 'a, {
171			SendThunk::new(move || a)
172		}
173
174		/// Defers a computation that returns a `SendThunk`.
175		#[document_signature]
176		///
177		#[document_parameters("The thunk that returns a `SendThunk`.")]
178		///
179		#[document_returns("A new `SendThunk` instance.")]
180		///
181		#[document_examples]
182		///
183		/// ```
184		/// use fp_library::types::*;
185		///
186		/// let thunk = SendThunk::defer(|| SendThunk::pure(42));
187		/// assert_eq!(thunk.evaluate(), 42);
188		/// ```
189		#[inline]
190		pub fn defer(f: impl FnOnce() -> SendThunk<'a, A> + Send + 'a) -> Self {
191			SendThunk::new(move || f().evaluate())
192		}
193
194		/// Monadic bind: chains computations.
195		///
196		/// Note: Each `bind` adds to the call stack. For deep recursion,
197		/// consider converting to [`Trampoline`](crate::types::Trampoline).
198		#[document_signature]
199		///
200		#[document_type_parameters("The type of the result of the new computation.")]
201		///
202		#[document_parameters("The function to apply to the result of the computation.")]
203		///
204		#[document_returns("A new `SendThunk` instance representing the chained computation.")]
205		///
206		#[document_examples]
207		///
208		/// ```
209		/// use fp_library::types::*;
210		///
211		/// let thunk = SendThunk::pure(21).bind(|x| SendThunk::pure(x * 2));
212		/// assert_eq!(thunk.evaluate(), 42);
213		/// ```
214		#[inline]
215		pub fn bind<B: 'a>(
216			self,
217			f: impl FnOnce(A) -> SendThunk<'a, B> + Send + 'a,
218		) -> SendThunk<'a, B> {
219			SendThunk::new(move || {
220				let a = (self.0)();
221				let thunk_b = f(a);
222				(thunk_b.0)()
223			})
224		}
225
226		/// Functor map: transforms the result.
227		#[document_signature]
228		///
229		#[document_type_parameters("The type of the result of the transformation.")]
230		///
231		#[document_parameters("The function to apply to the result of the computation.")]
232		///
233		#[document_returns("A new `SendThunk` instance with the transformed result.")]
234		///
235		#[document_examples]
236		///
237		/// ```
238		/// use fp_library::types::*;
239		///
240		/// let thunk = SendThunk::pure(21).map(|x| x * 2);
241		/// assert_eq!(thunk.evaluate(), 42);
242		/// ```
243		#[inline]
244		pub fn map<B: 'a>(
245			self,
246			f: impl FnOnce(A) -> B + Send + 'a,
247		) -> SendThunk<'a, B> {
248			SendThunk::new(move || f((self.0)()))
249		}
250
251		/// Forces evaluation and returns the result.
252		#[document_signature]
253		///
254		#[document_returns("The result of the computation.")]
255		///
256		#[document_examples]
257		///
258		/// ```
259		/// use fp_library::types::*;
260		///
261		/// let thunk = SendThunk::pure(42);
262		/// assert_eq!(thunk.evaluate(), 42);
263		/// ```
264		#[inline]
265		pub fn evaluate(self) -> A {
266			(self.0)()
267		}
268
269		/// Performs tail-recursive monadic computation.
270		///
271		/// The step function `f` is called in a loop, avoiding stack growth.
272		/// Each iteration evaluates `f(state)` and inspects the resulting
273		/// [`ControlFlow`]: `ControlFlow::Continue(next)` continues with `next`, while
274		/// `ControlFlow::Break(a)` breaks out and returns `a`.
275		///
276		/// # Step Function
277		///
278		/// The function `f` is bounded by `Fn`, so it is callable multiple
279		/// times by shared reference. Each iteration of the loop calls `f`
280		/// without consuming it, so no `Clone` bound is needed.
281		#[document_signature]
282		///
283		#[document_type_parameters("The type of the loop state.")]
284		///
285		#[document_parameters(
286			"The step function that produces the next state or the final result.",
287			"The initial state."
288		)]
289		///
290		#[document_returns("A `SendThunk` that, when evaluated, runs the tail-recursive loop.")]
291		///
292		#[document_examples]
293		///
294		/// ```
295		/// use {
296		/// 	core::ops::ControlFlow,
297		/// 	fp_library::types::*,
298		/// };
299		///
300		/// let result = SendThunk::tail_rec_m(
301		/// 	|x| {
302		/// 		SendThunk::pure(
303		/// 			if x < 1000 { ControlFlow::Continue(x + 1) } else { ControlFlow::Break(x) },
304		/// 		)
305		/// 	},
306		/// 	0,
307		/// );
308		/// assert_eq!(result.evaluate(), 1000);
309		/// ```
310		pub fn tail_rec_m<S>(
311			f: impl Fn(S) -> SendThunk<'a, ControlFlow<A, S>> + Send + 'a,
312			initial: S,
313		) -> Self
314		where
315			S: Send + 'a, {
316			SendThunk::new(move || {
317				let mut state = initial;
318				loop {
319					match f(state).evaluate() {
320						ControlFlow::Break(a) => return a,
321						ControlFlow::Continue(next) => state = next,
322					}
323				}
324			})
325		}
326
327		/// Arc-wrapped version of [`tail_rec_m`](SendThunk::tail_rec_m).
328		///
329		/// Wraps the closure in [`Arc`] internally so it can be shared
330		/// across thread boundaries. The step function must be `Send + Sync`
331		/// (rather than just `Send` as in `tail_rec_m`).
332		#[document_signature]
333		///
334		#[document_type_parameters("The type of the loop state.")]
335		///
336		#[document_parameters(
337			"The step function that produces the next state or the final result.",
338			"The initial state."
339		)]
340		///
341		#[document_returns("A `SendThunk` that, when evaluated, runs the tail-recursive loop.")]
342		///
343		#[document_examples]
344		///
345		/// ```
346		/// use {
347		/// 	core::ops::ControlFlow,
348		/// 	fp_library::types::*,
349		/// 	std::sync::{
350		/// 		Arc,
351		/// 		atomic::{
352		/// 			AtomicUsize,
353		/// 			Ordering,
354		/// 		},
355		/// 	},
356		/// };
357		///
358		/// let counter = Arc::new(AtomicUsize::new(0));
359		/// let counter_clone = Arc::clone(&counter);
360		/// let result = SendThunk::arc_tail_rec_m(
361		/// 	move |x| {
362		/// 		counter_clone.fetch_add(1, Ordering::SeqCst);
363		/// 		SendThunk::pure(
364		/// 			if x < 100 { ControlFlow::Continue(x + 1) } else { ControlFlow::Break(x) },
365		/// 		)
366		/// 	},
367		/// 	0,
368		/// );
369		/// assert_eq!(result.evaluate(), 100);
370		/// assert_eq!(counter.load(Ordering::SeqCst), 101);
371		/// ```
372		pub fn arc_tail_rec_m<S>(
373			f: impl Fn(S) -> SendThunk<'a, ControlFlow<A, S>> + Send + Sync + 'a,
374			initial: S,
375		) -> Self
376		where
377			S: Send + 'a, {
378			let f = Arc::new(f);
379			let wrapper = move |s: S| {
380				let f = Arc::clone(&f);
381				f(s)
382			};
383			Self::tail_rec_m(wrapper, initial)
384		}
385
386		/// Converts this `SendThunk` into a memoized [`ArcLazy`] value.
387		///
388		/// Unlike [`Thunk::into_arc_lazy`](crate::types::Thunk::into_arc_lazy), this
389		/// does **not** evaluate eagerly. The inner `Send` closure is passed
390		/// directly into `ArcLazy::new`, so evaluation is deferred until the
391		/// `ArcLazy` is first accessed.
392		#[document_signature]
393		///
394		#[document_returns("A thread-safe `ArcLazy` that evaluates this thunk on first access.")]
395		///
396		#[document_examples]
397		///
398		/// ```
399		/// use fp_library::types::*;
400		///
401		/// let thunk = SendThunk::new(|| 42);
402		/// let lazy = thunk.into_arc_lazy();
403		/// assert_eq!(*lazy.evaluate(), 42);
404		/// ```
405		#[inline]
406		pub fn into_arc_lazy(self) -> ArcLazy<'a, A>
407		where
408			A: Send + Sync, {
409			self.into()
410		}
411	}
412
413	#[document_type_parameters("The lifetime of the computation.", "The type of the value.")]
414	impl<'a, A: 'a> From<Thunk<'a, A>> for SendThunk<'a, A>
415	where
416		A: Send,
417	{
418		/// Converts a [`Thunk`] into a [`SendThunk`].
419		///
420		/// The `Thunk` closure is not `Send`, so the conversion eagerly
421		/// evaluates it and wraps the owned result in a new `SendThunk`.
422		#[document_signature]
423		#[document_parameters("The thunk to convert.")]
424		#[document_returns("A new `SendThunk` wrapping the evaluated result.")]
425		#[document_examples]
426		///
427		/// ```
428		/// use fp_library::types::*;
429		/// let thunk = Thunk::pure(42);
430		/// let send_thunk = SendThunk::from(thunk);
431		/// assert_eq!(send_thunk.evaluate(), 42);
432		/// ```
433		fn from(thunk: Thunk<'a, A>) -> Self {
434			SendThunk::pure(thunk.evaluate())
435		}
436	}
437
438	impl_kind! {
439		for SendThunkBrand {
440			type Of<'a, A: 'a>: 'a = SendThunk<'a, A>;
441		}
442	}
443
444	impl Foldable for SendThunkBrand {
445		/// Folds the `SendThunk` from the right.
446		#[document_signature]
447		///
448		#[document_type_parameters(
449			"The lifetime of the computation.",
450			"The brand of the cloneable function to use.",
451			"The type of the elements in the structure.",
452			"The type of the accumulator."
453		)]
454		///
455		#[document_parameters(
456			"The function to apply to each element and the accumulator.",
457			"The initial value of the accumulator.",
458			"The `SendThunk` to fold."
459		)]
460		///
461		#[document_returns("The final accumulator value.")]
462		#[document_examples]
463		///
464		/// ```
465		/// use fp_library::{
466		/// 	brands::*,
467		/// 	functions::*,
468		/// 	types::*,
469		/// };
470		///
471		/// let thunk = SendThunk::pure(10);
472		/// let result =
473		/// 	explicit::fold_right::<RcFnBrand, SendThunkBrand, _, _, _, _>(|a, b| a + b, 5, thunk);
474		/// assert_eq!(result, 15);
475		/// ```
476		fn fold_right<'a, FnBrand, A: 'a + Clone, B: 'a>(
477			func: impl Fn(A, B) -> B + 'a,
478			initial: B,
479			fa: Apply!(<Self as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, A>),
480		) -> B
481		where
482			FnBrand: CloneFn + 'a, {
483			func(fa.evaluate(), initial)
484		}
485
486		/// Folds the `SendThunk` from the left.
487		#[document_signature]
488		///
489		#[document_type_parameters(
490			"The lifetime of the computation.",
491			"The brand of the cloneable function to use.",
492			"The type of the elements in the structure.",
493			"The type of the accumulator."
494		)]
495		///
496		#[document_parameters(
497			"The function to apply to the accumulator and each element.",
498			"The initial value of the accumulator.",
499			"The `SendThunk` to fold."
500		)]
501		///
502		#[document_returns("The final accumulator value.")]
503		#[document_examples]
504		///
505		/// ```
506		/// use fp_library::{
507		/// 	brands::*,
508		/// 	functions::*,
509		/// 	types::*,
510		/// };
511		///
512		/// let thunk = SendThunk::pure(10);
513		/// let result =
514		/// 	explicit::fold_left::<RcFnBrand, SendThunkBrand, _, _, _, _>(|b, a| b + a, 5, thunk);
515		/// assert_eq!(result, 15);
516		/// ```
517		fn fold_left<'a, FnBrand, A: 'a + Clone, B: 'a>(
518			func: impl Fn(B, A) -> B + 'a,
519			initial: B,
520			fa: Apply!(<Self as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, A>),
521		) -> B
522		where
523			FnBrand: CloneFn + 'a, {
524			func(initial, fa.evaluate())
525		}
526
527		/// Maps the value to a monoid and returns it.
528		#[document_signature]
529		///
530		#[document_type_parameters(
531			"The lifetime of the computation.",
532			"The brand of the cloneable function to use.",
533			"The type of the elements in the structure.",
534			"The type of the monoid."
535		)]
536		///
537		#[document_parameters("The mapping function.", "The `SendThunk` to fold.")]
538		///
539		#[document_returns("The monoid value.")]
540		///
541		#[document_examples]
542		///
543		/// ```
544		/// use fp_library::{
545		/// 	brands::*,
546		/// 	functions::*,
547		/// 	types::*,
548		/// };
549		///
550		/// let thunk = SendThunk::pure(10);
551		/// let result =
552		/// 	explicit::fold_map::<RcFnBrand, SendThunkBrand, _, _, _, _>(|a: i32| a.to_string(), thunk);
553		/// assert_eq!(result, "10");
554		/// ```
555		fn fold_map<'a, FnBrand, A: 'a + Clone, M>(
556			func: impl Fn(A) -> M + 'a,
557			fa: Apply!(<Self as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, A>),
558		) -> M
559		where
560			M: Monoid + 'a,
561			FnBrand: CloneFn + 'a, {
562			func(fa.evaluate())
563		}
564	}
565
566	impl WithIndex for SendThunkBrand {
567		type Index = ();
568	}
569
570	impl FoldableWithIndex for SendThunkBrand {
571		/// Folds the send thunk using a monoid, providing the index `()`.
572		#[document_signature]
573		#[document_type_parameters(
574			"The lifetime of the computation.",
575			"The brand of the cloneable function to use.",
576			"The type of the value inside the send thunk.",
577			"The monoid type."
578		)]
579		#[document_parameters(
580			"The function to apply to the value and its index.",
581			"The send thunk to fold."
582		)]
583		#[document_returns("The monoid value.")]
584		#[document_examples]
585		///
586		/// ```
587		/// use fp_library::{
588		/// 	brands::*,
589		/// 	classes::foldable_with_index::FoldableWithIndex,
590		/// 	types::*,
591		/// };
592		///
593		/// let thunk = SendThunk::pure(5);
594		/// let result = <SendThunkBrand as FoldableWithIndex>::fold_map_with_index::<RcFnBrand, _, _>(
595		/// 	|_, x: i32| x.to_string(),
596		/// 	thunk,
597		/// );
598		/// assert_eq!(result, "5");
599		/// ```
600		fn fold_map_with_index<'a, FnBrand, A: 'a + Clone, R: Monoid>(
601			f: impl Fn((), A) -> R + 'a,
602			fa: Apply!(<Self as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, A>),
603		) -> R
604		where
605			FnBrand: LiftFn + 'a, {
606			f((), fa.evaluate())
607		}
608	}
609
610	#[document_type_parameters(
611		"The lifetime of the computation.",
612		"The type of the value produced by the computation."
613	)]
614	impl<'a, A: Send + 'a> SendDeferrable<'a> for SendThunk<'a, A> {
615		/// Creates a `SendThunk` from a thread-safe computation that produces it.
616		#[document_signature]
617		///
618		#[document_parameters("A thread-safe thunk that produces the send thunk.")]
619		///
620		#[document_returns("The deferred send thunk.")]
621		///
622		#[document_examples]
623		///
624		/// ```
625		/// use fp_library::{
626		/// 	classes::SendDeferrable,
627		/// 	types::*,
628		/// };
629		///
630		/// let task: SendThunk<i32> = SendDeferrable::send_defer(|| SendThunk::pure(42));
631		/// assert_eq!(task.evaluate(), 42);
632		/// ```
633		fn send_defer(f: impl FnOnce() -> Self + Send + 'a) -> Self
634		where
635			Self: Sized, {
636			SendThunk::defer(f)
637		}
638	}
639
640	#[document_type_parameters(
641		"The lifetime of the computation.",
642		"The type of the value produced by the computation."
643	)]
644	impl<'a, A: Semigroup + Send + 'a> Semigroup for SendThunk<'a, A> {
645		/// Combines two `SendThunk`s by combining their results.
646		#[document_signature]
647		///
648		#[document_parameters("The first `SendThunk`.", "The second `SendThunk`.")]
649		///
650		#[document_returns("A new `SendThunk` containing the combined result.")]
651		///
652		#[document_examples]
653		///
654		/// ```
655		/// use fp_library::{
656		/// 	classes::*,
657		/// 	functions::*,
658		/// 	types::*,
659		/// };
660		///
661		/// let t1 = SendThunk::pure("Hello".to_string());
662		/// let t2 = SendThunk::pure(" World".to_string());
663		/// let t3 = append::<_>(t1, t2);
664		/// assert_eq!(t3.evaluate(), "Hello World");
665		/// ```
666		fn append(
667			a: Self,
668			b: Self,
669		) -> Self {
670			SendThunk::new(move || Semigroup::append(a.evaluate(), b.evaluate()))
671		}
672	}
673
674	#[document_type_parameters(
675		"The lifetime of the computation.",
676		"The type of the value produced by the computation."
677	)]
678	impl<'a, A: Monoid + Send + 'a> Monoid for SendThunk<'a, A> {
679		/// Returns the identity `SendThunk`.
680		#[document_signature]
681		///
682		#[document_returns("A `SendThunk` producing the identity value of `A`.")]
683		///
684		#[document_examples]
685		///
686		/// ```
687		/// use fp_library::{
688		/// 	classes::*,
689		/// 	types::*,
690		/// };
691		///
692		/// let t: SendThunk<String> = SendThunk::empty();
693		/// assert_eq!(t.evaluate(), "");
694		/// ```
695		fn empty() -> Self {
696			SendThunk::new(|| Monoid::empty())
697		}
698	}
699
700	#[document_type_parameters(
701		"The lifetime of the computation.",
702		"The type of the computed value."
703	)]
704	#[document_parameters("The send thunk to format.")]
705	impl<'a, A> fmt::Debug for SendThunk<'a, A> {
706		/// Formats the send thunk without evaluating it.
707		#[document_signature]
708		#[document_parameters("The formatter.")]
709		#[document_returns("The formatting result.")]
710		#[document_examples]
711		///
712		/// ```
713		/// use fp_library::types::*;
714		/// let thunk = SendThunk::pure(42);
715		/// assert_eq!(format!("{:?}", thunk), "SendThunk(<unevaluated>)");
716		/// ```
717		fn fmt(
718			&self,
719			f: &mut fmt::Formatter<'_>,
720		) -> fmt::Result {
721			f.write_str("SendThunk(<unevaluated>)")
722		}
723	}
724}
725pub use inner::*;
726
727#[cfg(test)]
728#[expect(clippy::expect_used, reason = "Tests use panicking operations for brevity and clarity")]
729mod tests {
730	use {
731		super::*,
732		crate::classes::{
733			monoid::empty,
734			semigroup::append,
735		},
736		quickcheck_macros::quickcheck,
737	};
738
739	#[test]
740	fn test_send_thunk_pure_and_evaluate() {
741		let thunk = SendThunk::pure(42);
742		assert_eq!(thunk.evaluate(), 42);
743	}
744
745	#[test]
746	fn test_send_thunk_new() {
747		let thunk = SendThunk::new(|| 1 + 2);
748		assert_eq!(thunk.evaluate(), 3);
749	}
750
751	#[test]
752	fn test_send_thunk_map() {
753		let thunk = SendThunk::pure(10).map(|x| x * 3);
754		assert_eq!(thunk.evaluate(), 30);
755	}
756
757	#[test]
758	fn test_send_thunk_bind() {
759		let thunk = SendThunk::pure(5).bind(|x| SendThunk::pure(x + 10));
760		assert_eq!(thunk.evaluate(), 15);
761	}
762
763	#[test]
764	fn test_send_thunk_defer() {
765		let thunk = SendThunk::defer(|| SendThunk::pure(99));
766		assert_eq!(thunk.evaluate(), 99);
767	}
768
769	#[test]
770	fn test_send_thunk_into_arc_lazy() {
771		let thunk = SendThunk::new(|| 42);
772		let lazy = thunk.into_arc_lazy();
773		assert_eq!(*lazy.evaluate(), 42);
774		// Second access returns cached value.
775		assert_eq!(*lazy.evaluate(), 42);
776	}
777
778	#[test]
779	fn test_send_thunk_semigroup() {
780		let t1 = SendThunk::pure("Hello".to_string());
781		let t2 = SendThunk::pure(" World".to_string());
782		let t3 = append(t1, t2);
783		assert_eq!(t3.evaluate(), "Hello World");
784	}
785
786	#[test]
787	fn test_send_thunk_monoid() {
788		let t: SendThunk<String> = empty();
789		assert_eq!(t.evaluate(), "");
790	}
791
792	#[test]
793	fn test_send_thunk_from_thunk() {
794		let thunk = crate::types::Thunk::pure(42);
795		let send_thunk = SendThunk::from(thunk);
796		assert_eq!(send_thunk.evaluate(), 42);
797	}
798
799	#[test]
800	fn test_send_thunk_debug() {
801		let thunk = SendThunk::pure(42);
802		assert_eq!(format!("{:?}", thunk), "SendThunk(<unevaluated>)");
803	}
804
805	#[test]
806	fn test_send_thunk_is_send() {
807		fn assert_send<T: Send>() {}
808		assert_send::<SendThunk<'static, i32>>();
809	}
810
811	#[test]
812	fn test_send_thunk_send_deferrable() {
813		use crate::classes::SendDeferrable;
814		let task: SendThunk<i32> = SendDeferrable::send_defer(|| SendThunk::pure(42));
815		assert_eq!(task.evaluate(), 42);
816	}
817
818	/// Tests that a `SendThunk` can be sent to another thread and evaluated there.
819	///
820	/// Verifies that `SendThunk` satisfies the `Send` bound by moving it across a
821	/// thread boundary via `std::thread::spawn`.
822	#[test]
823	fn test_send_thunk_cross_thread() {
824		let thunk = SendThunk::new(|| 42 * 2);
825		let handle = std::thread::spawn(move || thunk.evaluate());
826		let result = handle.join().expect("thread should not panic");
827		assert_eq!(result, 84);
828	}
829
830	/// Tests that a mapped `SendThunk` evaluates correctly on another thread.
831	#[test]
832	fn test_send_thunk_cross_thread_with_map() {
833		let thunk = SendThunk::pure(10).map(|x| x + 5).map(|x| x * 3);
834		let handle = std::thread::spawn(move || thunk.evaluate());
835		let result = handle.join().expect("thread should not panic");
836		assert_eq!(result, 45);
837	}
838
839	/// Tests that a bound `SendThunk` evaluates correctly on another thread.
840	#[test]
841	fn test_send_thunk_cross_thread_with_bind() {
842		let thunk = SendThunk::pure(7).bind(|x| SendThunk::pure(x * 6));
843		let handle = std::thread::spawn(move || thunk.evaluate());
844		let result = handle.join().expect("thread should not panic");
845		assert_eq!(result, 42);
846	}
847
848	#[test]
849	fn test_send_thunk_tail_rec_m() {
850		use core::ops::ControlFlow;
851		let result = SendThunk::tail_rec_m(
852			|x| {
853				SendThunk::pure(
854					if x < 1000 { ControlFlow::Continue(x + 1) } else { ControlFlow::Break(x) },
855				)
856			},
857			0,
858		);
859		assert_eq!(result.evaluate(), 1000);
860	}
861
862	#[test]
863	fn test_send_thunk_tail_rec_m_stack_safety() {
864		use core::ops::ControlFlow;
865		let iterations: i64 = 200_000;
866		let result = SendThunk::tail_rec_m(
867			|acc| {
868				SendThunk::pure(
869					if acc < iterations {
870						ControlFlow::Continue(acc + 1)
871					} else {
872						ControlFlow::Break(acc)
873					},
874				)
875			},
876			0i64,
877		);
878		assert_eq!(result.evaluate(), iterations);
879	}
880
881	#[test]
882	fn test_send_thunk_arc_tail_rec_m() {
883		use {
884			core::ops::ControlFlow,
885			std::sync::{
886				Arc,
887				atomic::{
888					AtomicUsize,
889					Ordering,
890				},
891			},
892		};
893		let counter = Arc::new(AtomicUsize::new(0));
894		let counter_clone = Arc::clone(&counter);
895		let result = SendThunk::arc_tail_rec_m(
896			move |x| {
897				counter_clone.fetch_add(1, Ordering::SeqCst);
898				SendThunk::pure(
899					if x < 100 { ControlFlow::Continue(x + 1) } else { ControlFlow::Break(x) },
900				)
901			},
902			0,
903		);
904		assert_eq!(result.evaluate(), 100);
905		assert_eq!(counter.load(Ordering::SeqCst), 101);
906	}
907
908	#[test]
909	fn test_send_thunk_fold_right() {
910		use crate::{
911			brands::{
912				RcFnBrand,
913				SendThunkBrand,
914			},
915			functions::explicit,
916		};
917		let thunk = SendThunk::pure(10);
918		let result =
919			explicit::fold_right::<RcFnBrand, SendThunkBrand, _, _, _, _>(|a, b| a + b, 5, thunk);
920		assert_eq!(result, 15);
921	}
922
923	#[test]
924	fn test_send_thunk_fold_left() {
925		use crate::{
926			brands::{
927				RcFnBrand,
928				SendThunkBrand,
929			},
930			functions::explicit,
931		};
932		let thunk = SendThunk::pure(10);
933		let result =
934			explicit::fold_left::<RcFnBrand, SendThunkBrand, _, _, _, _>(|b, a| b + a, 5, thunk);
935		assert_eq!(result, 15);
936	}
937
938	#[test]
939	fn test_send_thunk_fold_map() {
940		use crate::{
941			brands::{
942				RcFnBrand,
943				SendThunkBrand,
944			},
945			functions::explicit,
946		};
947		let thunk = SendThunk::pure(10);
948		let result = explicit::fold_map::<RcFnBrand, SendThunkBrand, _, _, _, _>(
949			|a: i32| a.to_string(),
950			thunk,
951		);
952		assert_eq!(result, "10");
953	}
954
955	#[test]
956	fn test_send_thunk_fold_map_with_index() {
957		use crate::{
958			brands::{
959				RcFnBrand,
960				SendThunkBrand,
961			},
962			classes::foldable_with_index::FoldableWithIndex,
963		};
964		let thunk = SendThunk::pure(5);
965		let result = <SendThunkBrand as FoldableWithIndex>::fold_map_with_index::<RcFnBrand, _, _>(
966			|_, x: i32| x.to_string(),
967			thunk,
968		);
969		assert_eq!(result, "5");
970	}
971
972	#[test]
973	fn test_send_thunk_foldable_with_index_receives_unit_index() {
974		use crate::{
975			brands::{
976				RcFnBrand,
977				SendThunkBrand,
978			},
979			classes::foldable_with_index::FoldableWithIndex,
980		};
981		let thunk = SendThunk::pure(42);
982		let result = <SendThunkBrand as FoldableWithIndex>::fold_map_with_index::<RcFnBrand, _, _>(
983			|idx, x: i32| {
984				assert_eq!(idx, ());
985				vec![x]
986			},
987			thunk,
988		);
989		assert_eq!(result, vec![42]);
990	}
991
992	#[test]
993	fn test_send_thunk_foldable_consistency() {
994		use crate::{
995			brands::{
996				RcFnBrand,
997				SendThunkBrand,
998			},
999			classes::foldable_with_index::FoldableWithIndex,
1000			functions::explicit,
1001		};
1002		let f = |a: i32| a.to_string();
1003		let t1 = SendThunk::pure(7);
1004		let t2 = SendThunk::pure(7);
1005		// fold_map(f, fa) = fold_map_with_index(|_, a| f(a), fa)
1006		assert_eq!(
1007			explicit::fold_map::<RcFnBrand, SendThunkBrand, _, _, _, _>(f, t1),
1008			<SendThunkBrand as FoldableWithIndex>::fold_map_with_index::<RcFnBrand, _, _>(
1009				|_, a| f(a),
1010				t2
1011			),
1012		);
1013	}
1014
1015	// QuickCheck Law Tests
1016
1017	// Functor Laws
1018
1019	/// Functor identity: `send_thunk.map(identity).evaluate() == send_thunk.evaluate()`.
1020	#[quickcheck]
1021	fn functor_identity(x: i32) -> bool {
1022		SendThunk::pure(x).map(|a| a).evaluate() == x
1023	}
1024
1025	/// Functor composition: `send_thunk.map(f).map(g) == send_thunk.map(|x| g(f(x)))`.
1026	#[quickcheck]
1027	fn functor_composition(x: i32) -> bool {
1028		let f = |a: i32| a.wrapping_add(1);
1029		let g = |a: i32| a.wrapping_mul(2);
1030		let lhs = SendThunk::pure(x).map(f).map(g).evaluate();
1031		let rhs = SendThunk::pure(x).map(move |a| g(f(a))).evaluate();
1032		lhs == rhs
1033	}
1034
1035	// Monad Laws
1036
1037	/// Monad left identity: `SendThunk::pure(a).bind(f) == f(a)`.
1038	#[quickcheck]
1039	fn monad_left_identity(a: i32) -> bool {
1040		let f = |x: i32| SendThunk::pure(x.wrapping_mul(2));
1041		let lhs = SendThunk::pure(a).bind(f).evaluate();
1042		let rhs = f(a).evaluate();
1043		lhs == rhs
1044	}
1045
1046	/// Monad right identity: `send_thunk.bind(SendThunk::pure) == send_thunk`.
1047	#[quickcheck]
1048	fn monad_right_identity(x: i32) -> bool {
1049		let lhs = SendThunk::pure(x).bind(SendThunk::pure).evaluate();
1050		lhs == x
1051	}
1052
1053	/// Monad associativity: `m.bind(f).bind(g) == m.bind(|x| f(x).bind(g))`.
1054	#[quickcheck]
1055	fn monad_associativity(x: i32) -> bool {
1056		let f = |a: i32| SendThunk::pure(a.wrapping_add(1));
1057		let g = |a: i32| SendThunk::pure(a.wrapping_mul(3));
1058		let lhs = SendThunk::pure(x).bind(f).bind(g).evaluate();
1059		let rhs = SendThunk::pure(x).bind(move |a| f(a).bind(g)).evaluate();
1060		lhs == rhs
1061	}
1062}