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