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