Skip to main content

fp_library/types/
try_lazy.rs

1//! Memoized lazy evaluation for fallible computations.
2//!
3//! Computes a [`Result`] at most once and caches either the success value or error. All clones share the same cache. Available in both single-threaded [`RcTryLazy`] and thread-safe [`ArcTryLazy`] variants.
4
5#[fp_macros::document_module]
6mod inner {
7	use crate::{
8		brands::TryLazyBrand,
9		classes::{Deferrable, SendDeferrable},
10		impl_kind,
11		kinds::*,
12		types::{ArcLazyConfig, Lazy, LazyConfig, RcLazyConfig, TryThunk, TryTrampoline},
13	};
14	use fp_macros::{document_fields, document_parameters, document_type_parameters};
15
16	/// A lazily-computed, memoized value that may fail.
17	///
18	/// The computation runs at most once. If it succeeds, the value is cached.
19	/// If it fails, the error is cached. Subsequent accesses return the cached result.
20	///
21	#[document_type_parameters(
22		"The lifetime of the computation.",
23		"The type of the computed value.",
24		"The type of the error.",
25		"The memoization configuration."
26	)]
27	///
28	/// ### Higher-Kinded Type Representation
29	///
30	/// The higher-kinded representation of this type constructor is [`TryLazyBrand<E, Config>`](crate::brands::TryLazyBrand),
31	/// which is parameterized by both the error type and the `LazyConfig`, and is polymorphic over the success value type.
32	///
33	/// ### Fields
34	///
35	#[document_fields("The internal lazy cell.")]
36	pub struct TryLazy<'a, A, E, Config: LazyConfig = RcLazyConfig>(
37		pub(crate) Config::TryLazy<'a, A, E>,
38	)
39	where
40		A: 'a,
41		E: 'a;
42
43	/// ### Type Parameters
44	///
45	#[document_type_parameters(
46		"The lifetime of the computation.",
47		"The type of the computed value.",
48		"The type of the error.",
49		"The memoization configuration."
50	)]
51	#[document_parameters("The instance to clone.")]
52	impl<'a, A, E, Config: LazyConfig> Clone for TryLazy<'a, A, E, Config>
53	where
54		A: 'a,
55		E: 'a,
56	{
57		/// ### Type Signature
58		///
59		#[document_signature]
60		fn clone(&self) -> Self {
61			Self(self.0.clone())
62		}
63	}
64
65	/// ### Type Parameters
66	///
67	#[document_type_parameters(
68		"The lifetime of the computation.",
69		"The type of the computed value.",
70		"The type of the error.",
71		"The memoization configuration."
72	)]
73	#[document_parameters("The `TryLazy` instance.")]
74	impl<'a, A, E, Config: LazyConfig> TryLazy<'a, A, E, Config>
75	where
76		A: 'a,
77		E: 'a,
78	{
79		/// Gets the memoized result, computing on first access.
80		///
81		/// ### Type Signature
82		///
83		#[document_signature]
84		///
85		/// ### Returns
86		///
87		/// A result containing a reference to the value or error.
88		///
89		/// ### Examples
90		///
91		/// ```
92		/// use fp_library::types::*;
93		///
94		/// let memo = TryLazy::<_, _, RcLazyConfig>::new(|| Ok::<i32, ()>(42));
95		/// assert_eq!(memo.evaluate(), Ok(&42));
96		/// ```
97		pub fn evaluate(&self) -> Result<&A, &E> {
98			Config::try_evaluate(&self.0)
99		}
100	}
101
102	/// ### Type Parameters
103	///
104	#[document_type_parameters(
105		"The lifetime of the computation.",
106		"The type of the computed value.",
107		"The type of the error."
108	)]
109	impl<'a, A, E> TryLazy<'a, A, E, RcLazyConfig>
110	where
111		A: 'a,
112		E: 'a,
113	{
114		/// Creates a new `TryLazy` that will run `f` on first access.
115		///
116		/// ### Type Signature
117		///
118		#[document_signature]
119		///
120		/// ### Type Parameters
121		///
122		#[document_type_parameters("The type of the initializer closure.")]
123		///
124		/// ### Parameters
125		///
126		#[document_parameters("The closure that produces the result.")]
127		///
128		/// ### Returns
129		///
130		/// A new `TryLazy` instance.
131		///
132		/// ### Examples
133		///
134		/// ```
135		/// use fp_library::types::*;
136		///
137		/// let memo = TryLazy::<_, _, RcLazyConfig>::new(|| Ok::<i32, ()>(42));
138		/// assert_eq!(memo.evaluate(), Ok(&42));
139		/// ```
140		pub fn new<F>(f: F) -> Self
141		where
142			F: FnOnce() -> Result<A, E> + 'a,
143		{
144			TryLazy(RcLazyConfig::try_lazy_new(Box::new(f)))
145		}
146	}
147
148	/// ### Type Parameters
149	///
150	#[document_type_parameters(
151		"The lifetime of the computation.",
152		"The type of the computed value.",
153		"The type of the error."
154	)]
155	impl<'a, A, E> From<TryThunk<'a, A, E>> for TryLazy<'a, A, E, RcLazyConfig> {
156		/// ### Type Signature
157		///
158		#[document_signature]
159		///
160		/// ### Parameters
161		///
162		#[document_parameters("The fallible thunk to convert.")]
163		fn from(eval: TryThunk<'a, A, E>) -> Self {
164			Self::new(move || eval.evaluate())
165		}
166	}
167
168	/// ### Type Parameters
169	///
170	#[document_type_parameters(
171		"The lifetime of the computation.",
172		"The type of the computed value.",
173		"The type of the error."
174	)]
175	impl<'a, A, E> From<TryTrampoline<A, E>> for TryLazy<'a, A, E, RcLazyConfig>
176	where
177		A: Send,
178		E: Send,
179	{
180		/// ### Type Signature
181		///
182		#[document_signature]
183		///
184		/// ### Parameters
185		///
186		#[document_parameters("The fallible trampoline to convert.")]
187		fn from(task: TryTrampoline<A, E>) -> Self {
188			Self::new(move || task.evaluate())
189		}
190	}
191
192	/// ### Type Parameters
193	///
194	#[document_type_parameters(
195		"The lifetime of the computation.",
196		"The type of the computed value.",
197		"The type of the error."
198	)]
199	impl<'a, A, E> From<Lazy<'a, A, ArcLazyConfig>> for TryLazy<'a, A, E, ArcLazyConfig>
200	where
201		A: Clone + Send + Sync + 'a,
202		E: Send + Sync + 'a,
203	{
204		/// ### Type Signature
205		///
206		#[document_signature]
207		///
208		/// ### Parameters
209		///
210		#[document_parameters("The thread-safe lazy value to convert.")]
211		fn from(memo: Lazy<'a, A, ArcLazyConfig>) -> Self {
212			Self::new(move || Ok(memo.evaluate().clone()))
213		}
214	}
215
216	/// ### Type Parameters
217	///
218	#[document_type_parameters(
219		"The lifetime of the computation.",
220		"The type of the computed value.",
221		"The type of the error."
222	)]
223	impl<'a, A, E> From<Lazy<'a, A, RcLazyConfig>> for TryLazy<'a, A, E, RcLazyConfig>
224	where
225		A: Clone + 'a,
226		E: 'a,
227	{
228		/// ### Type Signature
229		///
230		#[document_signature]
231		///
232		/// ### Parameters
233		///
234		#[document_parameters("The lazy value to convert.")]
235		fn from(memo: Lazy<'a, A, RcLazyConfig>) -> Self {
236			Self::new(move || Ok(memo.evaluate().clone()))
237		}
238	}
239
240	/// ### Type Parameters
241	///
242	#[document_type_parameters(
243		"The lifetime of the computation.",
244		"The type of the computed value."
245	)]
246	impl<'a, A> TryLazy<'a, A, String, RcLazyConfig>
247	where
248		A: 'a,
249	{
250		/// Creates a `TryLazy` that catches unwinds (panics).
251		///
252		/// ### Type Signature
253		///
254		#[document_signature]
255		///
256		/// ### Type Parameters
257		///
258		#[document_type_parameters("The type of the initializer closure.")]
259		///
260		/// ### Parameters
261		///
262		#[document_parameters("The closure that might panic.")]
263		///
264		/// ### Returns
265		///
266		/// A new `TryLazy` instance where panics are converted to `Err(String)`.
267		///
268		/// ### Examples
269		///
270		/// ```
271		/// use fp_library::types::*;
272		///
273		/// let memo = TryLazy::<_, String, RcLazyConfig>::catch_unwind(|| {
274		///     if true { panic!("oops") }
275		///     42
276		/// });
277		/// assert_eq!(memo.evaluate(), Err(&"oops".to_string()));
278		/// ```
279		pub fn catch_unwind<F>(f: F) -> Self
280		where
281			F: FnOnce() -> A + std::panic::UnwindSafe + 'a,
282		{
283			Self::new(move || {
284				std::panic::catch_unwind(f).map_err(|e| {
285					if let Some(s) = e.downcast_ref::<&str>() {
286						s.to_string()
287					} else if let Some(s) = e.downcast_ref::<String>() {
288						s.clone()
289					} else {
290						"Unknown panic".to_string()
291					}
292				})
293			})
294		}
295	}
296
297	/// ### Type Parameters
298	///
299	#[document_type_parameters(
300		"The lifetime of the computation.",
301		"The type of the computed value.",
302		"The type of the error."
303	)]
304	impl<'a, A, E> TryLazy<'a, A, E, ArcLazyConfig>
305	where
306		A: 'a,
307		E: 'a,
308	{
309		/// Creates a new `TryLazy` that will run `f` on first access.
310		///
311		/// ### Type Signature
312		///
313		#[document_signature]
314		///
315		/// ### Type Parameters
316		///
317		#[document_type_parameters("The type of the initializer closure.")]
318		///
319		/// ### Parameters
320		///
321		#[document_parameters("The closure that produces the result.")]
322		///
323		/// ### Returns
324		///
325		/// A new `TryLazy` instance.
326		///
327		/// ### Examples
328		///
329		/// ```
330		/// use fp_library::types::*;
331		///
332		/// let memo = TryLazy::<_, _, ArcLazyConfig>::new(|| Ok::<i32, ()>(42));
333		/// assert_eq!(memo.evaluate(), Ok(&42));
334		/// ```
335		pub fn new<F>(f: F) -> Self
336		where
337			F: FnOnce() -> Result<A, E> + Send + 'a,
338		{
339			TryLazy(ArcLazyConfig::try_lazy_new(Box::new(f)))
340		}
341	}
342
343	/// ### Type Parameters
344	///
345	#[document_type_parameters(
346		"The lifetime of the computation.",
347		"The type of the computed value.",
348		"The type of the error."
349	)]
350	impl<'a, A, E> Deferrable<'a> for TryLazy<'a, A, E, RcLazyConfig>
351	where
352		A: Clone + 'a,
353		E: Clone + 'a,
354	{
355		/// Defers a computation that produces a `TryLazy` value.
356		///
357		/// This flattens the nested structure: instead of `TryLazy<TryLazy<A, E>, E>`, we get `TryLazy<A, E>`.
358		/// The inner `TryLazy` is computed only when the outer `TryLazy` is evaluated.
359		///
360		/// ### Type Signature
361		///
362		#[document_signature]
363		///
364		/// ### Type Parameters
365		///
366		#[document_type_parameters("The type of the thunk.")]
367		///
368		/// ### Parameters
369		///
370		#[document_parameters("The thunk that produces the lazy value.")]
371		///
372		/// ### Returns
373		///
374		/// A new `TryLazy` value.
375		///
376		/// ### Examples
377		///
378		/// ```
379		/// use fp_library::{brands::*, classes::*, types::*, functions::*};
380		///
381		/// let lazy = TryLazy::<_, (), RcLazyConfig>::defer(|| RcTryLazy::new(|| Ok(42)));
382		/// assert_eq!(lazy.evaluate(), Ok(&42));
383		/// ```
384		fn defer<F>(f: F) -> Self
385		where
386			F: FnOnce() -> Self + 'a,
387			Self: Sized,
388		{
389			Self::new(move || f().evaluate().cloned().map_err(Clone::clone))
390		}
391	}
392
393	impl_kind! {
394		impl<E: 'static, Config: LazyConfig> for TryLazyBrand<E, Config> {
395			#[document_default]
396			type Of<'a, A: 'a>: 'a = TryLazy<'a, A, E, Config>;
397		}
398	}
399
400	/// ### Type Parameters
401	///
402	#[document_type_parameters(
403		"The lifetime of the computation.",
404		"The type of the computed value.",
405		"The type of the error."
406	)]
407	impl<'a, A, E> SendDeferrable<'a> for TryLazy<'a, A, E, ArcLazyConfig>
408	where
409		A: Clone + Send + Sync + 'a,
410		E: Clone + Send + Sync + 'a,
411	{
412		/// Defers a computation that produces a thread-safe `TryLazy` value.
413		///
414		/// This flattens the nested structure: instead of `ArcTryLazy<ArcTryLazy<A, E>, E>`, we get `ArcTryLazy<A, E>`.
415		/// The inner `TryLazy` is computed only when the outer `TryLazy` is evaluated.
416		///
417		/// ### Type Signature
418		///
419		#[document_signature]
420		///
421		/// ### Type Parameters
422		///
423		#[document_type_parameters("The type of the thunk.")]
424		///
425		/// ### Parameters
426		///
427		#[document_parameters("The thunk that produces the lazy value.")]
428		///
429		/// ### Returns
430		///
431		/// A new `ArcTryLazy` value.
432		///
433		/// ### Examples
434		///
435		/// ```
436		/// use fp_library::{brands::*, classes::*, types::*};
437		///
438		/// let lazy: ArcTryLazy<i32, ()> = ArcTryLazy::send_defer(|| ArcTryLazy::new(|| Ok(42)));
439		/// assert_eq!(lazy.evaluate(), Ok(&42));
440		/// ```
441		fn send_defer<F>(f: F) -> Self
442		where
443			F: FnOnce() -> Self + Send + Sync + 'a,
444			Self: Sized,
445		{
446			Self::new(move || f().evaluate().cloned().map_err(Clone::clone))
447		}
448	}
449
450	/// Single-threaded fallible memoization alias.
451	pub type RcTryLazy<'a, A, E> = TryLazy<'a, A, E, RcLazyConfig>;
452
453	/// Thread-safe fallible memoization alias.
454	pub type ArcTryLazy<'a, A, E> = TryLazy<'a, A, E, ArcLazyConfig>;
455}
456pub use inner::*;
457
458#[cfg(test)]
459mod tests {
460	use crate::types::{RcLazy, TryThunk, TryTrampoline};
461
462	use super::*;
463	use std::cell::RefCell;
464	use std::rc::Rc;
465
466	/// Tests that `TryLazy` caches successful results.
467	///
468	/// Verifies that the initializer is called only once for success.
469	#[test]
470	fn test_try_memo_caching_ok() {
471		let counter = Rc::new(RefCell::new(0));
472		let counter_clone = counter.clone();
473		let memo: RcTryLazy<i32, ()> = RcTryLazy::new(move || {
474			*counter_clone.borrow_mut() += 1;
475			Ok(42)
476		});
477
478		assert_eq!(*counter.borrow(), 0);
479		assert_eq!(memo.evaluate(), Ok(&42));
480		assert_eq!(*counter.borrow(), 1);
481		assert_eq!(memo.evaluate(), Ok(&42));
482		assert_eq!(*counter.borrow(), 1);
483	}
484
485	/// Tests that `TryLazy` caches error results.
486	///
487	/// Verifies that the initializer is called only once for error.
488	#[test]
489	fn test_try_memo_caching_err() {
490		let counter = Rc::new(RefCell::new(0));
491		let counter_clone = counter.clone();
492		let memo: RcTryLazy<i32, i32> = RcTryLazy::new(move || {
493			*counter_clone.borrow_mut() += 1;
494			Err(0)
495		});
496
497		assert_eq!(*counter.borrow(), 0);
498		assert_eq!(memo.evaluate(), Err(&0));
499		assert_eq!(*counter.borrow(), 1);
500		assert_eq!(memo.evaluate(), Err(&0));
501		assert_eq!(*counter.borrow(), 1);
502	}
503
504	/// Tests that `TryLazy` shares the cache across clones.
505	///
506	/// Verifies that clones see the same result.
507	#[test]
508	fn test_try_memo_sharing() {
509		let counter = Rc::new(RefCell::new(0));
510		let counter_clone = counter.clone();
511		let memo: RcTryLazy<i32, ()> = RcTryLazy::new(move || {
512			*counter_clone.borrow_mut() += 1;
513			Ok(42)
514		});
515		let shared = memo.clone();
516
517		assert_eq!(memo.evaluate(), Ok(&42));
518		assert_eq!(*counter.borrow(), 1);
519		assert_eq!(shared.evaluate(), Ok(&42));
520		assert_eq!(*counter.borrow(), 1);
521	}
522
523	/// Tests `catch_unwind`.
524	///
525	/// Verifies that panics are caught and converted to errors.
526	#[test]
527	fn test_catch_unwind() {
528		let memo = RcTryLazy::catch_unwind(|| {
529			if true {
530				panic!("oops")
531			}
532			42
533		});
534
535		match memo.evaluate() {
536			Err(e) => assert_eq!(e, "oops"),
537			Ok(_) => panic!("Should have failed"),
538		}
539	}
540
541	/// Tests creation from `TryThunk`.
542	#[test]
543	fn test_try_memo_from_try_eval() {
544		let eval = TryThunk::new(|| Ok::<i32, ()>(42));
545		let memo = RcTryLazy::from(eval);
546		assert_eq!(memo.evaluate(), Ok(&42));
547	}
548
549	/// Tests creation from `TryTrampoline`.
550	#[test]
551	fn test_try_memo_from_try_task() {
552		let task = TryTrampoline::<i32, ()>::ok(42);
553		let memo = RcTryLazy::from(task);
554		assert_eq!(memo.evaluate(), Ok(&42));
555	}
556
557	/// Tests conversion to TryLazy.
558	#[test]
559	fn test_try_memo_from_rc_memo() {
560		let memo = RcLazy::new(|| 42);
561		let try_memo: crate::types::RcTryLazy<i32, ()> = crate::types::RcTryLazy::from(memo);
562		assert_eq!(try_memo.evaluate(), Ok(&42));
563	}
564
565	/// Tests conversion to ArcTryLazy.
566	#[test]
567	fn test_try_memo_from_arc_memo() {
568		use crate::types::ArcLazy;
569		let memo = ArcLazy::new(|| 42);
570		let try_memo: crate::types::ArcTryLazy<i32, ()> = crate::types::ArcTryLazy::from(memo);
571		assert_eq!(try_memo.evaluate(), Ok(&42));
572	}
573
574	/// Tests SendDefer implementation.
575	#[test]
576	fn test_send_defer() {
577		use crate::classes::send_deferrable::send_defer;
578
579		let memo: ArcTryLazy<i32, ()> = send_defer(|| ArcTryLazy::new(|| Ok(42)));
580		assert_eq!(memo.evaluate(), Ok(&42));
581	}
582}