fp_library/types/lazy.rs
1//! Lazy value wrapper.
2//!
3//! This module defines the [`Lazy`] struct, which represents a lazily-computed, memoized value.
4//! It implements [`Semigroup`], [`Monoid`], and [`Defer`].
5
6use crate::{
7 Apply,
8 brands::LazyBrand,
9 classes::{
10 clonable_fn::ClonableFn, defer::Defer, monoid::Monoid, once::Once, semigroup::Semigroup,
11 },
12 impl_kind,
13 kinds::*,
14};
15
16/// Represents a lazily-computed, memoized value.
17///
18/// `Lazy` stores a computation (a thunk) that is executed only when the value is needed.
19/// The result is then cached (memoized) so that subsequent accesses return the same value
20/// without re-executing the computation.
21pub struct Lazy<'a, OnceBrand: Once, FnBrand: ClonableFn, A>(
22 pub Apply!(brand: OnceBrand, kind: Once, lifetimes: (), types: (A)),
23 pub Apply!(brand: FnBrand, kind: ClonableFn, lifetimes: ('a), types: ((), A)),
24);
25
26impl<'a, OnceBrand: Once, FnBrand: ClonableFn, A> Lazy<'a, OnceBrand, FnBrand, A> {
27 /// Creates a new `Lazy` value from a thunk.
28 ///
29 /// The thunk is wrapped in a clonable function (e.g., `Rc<dyn Fn() -> A>`) to allow
30 /// the `Lazy` value to be cloned.
31 ///
32 /// ### Type Signature
33 ///
34 /// `forall a. (() -> a) -> Lazy a`
35 ///
36 /// ### Type Parameters
37 ///
38 /// * `OnceBrand`: The brand of the once cell (e.g., `OnceCellBrand`).
39 /// * `FnBrand`: The brand of the clonable function (e.g., `RcFnBrand`).
40 /// * `A`: The type of the value.
41 ///
42 /// ### Parameters
43 ///
44 /// * `a`: The thunk that produces the value.
45 ///
46 /// ### Returns
47 ///
48 /// A new `Lazy` value.
49 ///
50 /// ### Examples
51 ///
52 /// ```
53 /// use fp_library::types::lazy::Lazy;
54 /// use fp_library::brands::RcFnBrand;
55 /// use fp_library::brands::OnceCellBrand;
56 /// use fp_library::classes::clonable_fn::ClonableFn;
57 ///
58 /// let lazy = Lazy::<OnceCellBrand, RcFnBrand, _>::new(<RcFnBrand as ClonableFn>::new(|_| 42));
59 /// ```
60 pub fn new(
61 a: Apply!(brand: FnBrand, kind: ClonableFn, lifetimes: ('a), types: ((), A))
62 ) -> Self {
63 Self(OnceBrand::new(), a)
64 }
65
66 /// Forces the evaluation of the thunk and returns the value.
67 ///
68 /// If the value has already been computed, the cached value is returned.
69 /// Requires `A: Clone` because the value is stored inside the `Lazy` struct and
70 /// must be cloned to be returned to the caller.
71 ///
72 /// ### Type Signature
73 ///
74 /// `forall a. Lazy a -> a`
75 ///
76 /// ### Type Parameters
77 ///
78 /// * `OnceBrand`: The brand of the once cell (e.g., `OnceCellBrand`).
79 /// * `FnBrand`: The brand of the clonable function (e.g., `RcFnBrand`).
80 /// * `A`: The type of the value.
81 ///
82 /// ### Parameters
83 ///
84 /// * `a`: The lazy value to force.
85 ///
86 /// ### Returns
87 ///
88 /// The computed value.
89 ///
90 /// ### Examples
91 ///
92 /// ```
93 /// use fp_library::types::lazy::Lazy;
94 /// use fp_library::brands::RcFnBrand;
95 /// use fp_library::brands::OnceCellBrand;
96 /// use fp_library::classes::clonable_fn::ClonableFn;
97 ///
98 /// let lazy = Lazy::<OnceCellBrand, RcFnBrand, _>::new(<RcFnBrand as ClonableFn>::new(|_| 42));
99 /// assert_eq!(Lazy::force(lazy), 42);
100 /// ```
101 pub fn force(a: Self) -> A
102 where
103 A: Clone,
104 {
105 <OnceBrand as Once>::get_or_init(&a.0, move || (a.1)(())).clone()
106 }
107}
108
109impl<'a, OnceBrand: Once, FnBrand: ClonableFn, A: Clone> Clone for Lazy<'a, OnceBrand, FnBrand, A>
110where
111 Apply!(brand: OnceBrand, kind: Once, lifetimes: (), types: (A)): Clone,
112{
113 fn clone(&self) -> Self {
114 Self(self.0.clone(), self.1.clone())
115 }
116}
117
118impl_kind! {
119 impl<OnceBrand: Once + 'static, FnBrand: ClonableFn + 'static>
120 for LazyBrand<OnceBrand, FnBrand>
121 {
122 type Of<'a, A: 'a>: 'a = Lazy<'a, OnceBrand, FnBrand, A>;
123 }
124}
125
126// Note: Lazy cannot implement Functor, Pointed, or Semimonad because these traits
127// require operations to work for all types A, but Lazy requires A: Clone to be
128// forced (memoized). Adding A: Clone bounds to the traits would restrict all
129// other implementations (e.g. Option<NonClone>), which is undesirable.
130//
131// Consequently, Lazy cannot implement Semiapplicative either, as it extends Functor.
132
133impl<'b, OnceBrand: 'b + Once, FnBrand: 'b + ClonableFn, A: Semigroup + Clone + 'b> Semigroup
134 for Lazy<'b, OnceBrand, FnBrand, A>
135where
136 Apply!(brand: OnceBrand, kind: Once, lifetimes: (), types: (A)): Clone,
137{
138 /// The result of combining the two values using the semigroup operation.
139 ///
140 /// This method combines two lazy values using the underlying type's `Semigroup` implementation.
141 /// The combination is itself lazy: the result is a new thunk that, when forced,
142 /// forces both input values and combines them.
143 ///
144 /// ### Type Signature
145 ///
146 /// `forall a. Semigroup a => (Lazy a, Lazy a) -> Lazy a`
147 ///
148 /// ### Parameters
149 ///
150 /// * `a`: The first lazy value.
151 /// * `b`: The second lazy value.
152 ///
153 /// ### Returns
154 ///
155 /// A new lazy value that combines the results.
156 ///
157 /// ### Examples
158 ///
159 /// ```
160 /// use fp_library::types::lazy::Lazy;
161 /// use fp_library::brands::RcFnBrand;
162 /// use fp_library::brands::OnceCellBrand;
163 /// use fp_library::classes::clonable_fn::ClonableFn;
164 /// use fp_library::classes::semigroup::Semigroup;
165 /// use fp_library::types::string; // Import Semigroup impl for String
166 ///
167 /// let x = Lazy::<OnceCellBrand, RcFnBrand, _>::new(<RcFnBrand as ClonableFn>::new(|_| "Hello, ".to_string()));
168 /// let y = Lazy::<OnceCellBrand, RcFnBrand, _>::new(<RcFnBrand as ClonableFn>::new(|_| "World!".to_string()));
169 /// let z = Semigroup::append(x, y);
170 /// assert_eq!(Lazy::force(z), "Hello, World!".to_string());
171 /// ```
172 fn append(
173 a: Self,
174 b: Self,
175 ) -> Self {
176 Lazy::new(<FnBrand as ClonableFn>::new(move |_| {
177 Semigroup::append(Lazy::force(a.clone()), Lazy::force(b.clone()))
178 }))
179 }
180}
181
182impl<'b, OnceBrand: 'b + Once, FnBrand: 'b + ClonableFn, A: Monoid + Clone + 'b> Monoid
183 for Lazy<'b, OnceBrand, FnBrand, A>
184where
185 Apply!(brand: OnceBrand, kind: Once, lifetimes: (), types: (A)): Clone,
186{
187 /// The identity element.
188 ///
189 /// This method returns a lazy value that evaluates to the underlying type's identity element.
190 ///
191 /// ### Type Signature
192 ///
193 /// `forall a. Monoid a => () -> Lazy a`
194 ///
195 /// ### Returns
196 ///
197 /// A lazy value containing the identity element.
198 ///
199 /// ### Examples
200 ///
201 /// ```
202 /// use fp_library::types::lazy::Lazy;
203 /// use fp_library::brands::RcFnBrand;
204 /// use fp_library::brands::OnceCellBrand;
205 /// use fp_library::classes::monoid::Monoid;
206 /// use fp_library::types::string; // Import Monoid impl for String
207 ///
208 /// let x = Lazy::<OnceCellBrand, RcFnBrand, String>::empty();
209 /// assert_eq!(Lazy::force(x), "".to_string());
210 /// ```
211 fn empty() -> Self {
212 Lazy::new(<FnBrand as ClonableFn>::new(move |_| Monoid::empty()))
213 }
214}
215
216impl<'a, OnceBrand: Once + 'a, FnBrand: ClonableFn + 'a, A: Clone + 'a> Defer<'a>
217 for Lazy<'a, OnceBrand, FnBrand, A>
218{
219 /// Creates a value from a computation that produces the value.
220 ///
221 /// This method defers the construction of a `Lazy` value.
222 /// This allows creating a `Lazy` value from a computation that produces a `Lazy` value.
223 /// The outer computation is executed only when the result is forced.
224 ///
225 /// ### Type Signature
226 ///
227 /// `forall a. (() -> Lazy a) -> Lazy a`
228 /// ### Type Parameters
229 ///
230 /// * `FnBrand`: The brand of the clonable function wrapper.
231 ///
232 /// ### Parameters
233 ///
234 /// * `f`: A thunk (wrapped in a clonable function) that produces the value.
235 ///
236 /// ### Returns
237 ///
238 /// A new lazy value.
239 ///
240 /// ### Examples
241 ///
242 /// ```
243 /// use fp_library::types::lazy::Lazy;
244 /// use fp_library::brands::RcFnBrand;
245 /// use fp_library::brands::OnceCellBrand;
246 /// use fp_library::classes::clonable_fn::ClonableFn;
247 /// use fp_library::classes::defer::Defer;
248 /// use std::rc::Rc;
249 ///
250 /// let lazy = Lazy::<OnceCellBrand, RcFnBrand, _>::defer::<RcFnBrand>(
251 /// <RcFnBrand as ClonableFn>::new(|_| Lazy::new(<RcFnBrand as ClonableFn>::new(|_| 42)))
252 /// );
253 /// assert_eq!(Lazy::force(lazy), 42);
254 /// ```
255 fn defer<FnBrand_>(
256 f: Apply!(brand: FnBrand_, kind: ClonableFn, lifetimes: ('a), types: ((), Self))
257 ) -> Self
258 where
259 Self: Sized,
260 FnBrand_: ClonableFn + 'a,
261 {
262 Self::new(<FnBrand as ClonableFn>::new(move |_| Lazy::force(f(()))))
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use crate::{
270 brands::{OnceCellBrand, RcFnBrand},
271 classes::{clonable_fn::ClonableFn, defer::Defer},
272 };
273 use std::{cell::RefCell, rc::Rc};
274
275 /// Tests that `Lazy::force` memoizes the result.
276 #[test]
277 fn force_memoization() {
278 let counter = Rc::new(RefCell::new(0));
279 let counter_clone = counter.clone();
280
281 let lazy =
282 Lazy::<OnceCellBrand, RcFnBrand, _>::new(<RcFnBrand as ClonableFn>::new(move |_| {
283 *counter_clone.borrow_mut() += 1;
284 42
285 }));
286
287 assert_eq!(*counter.borrow(), 0);
288 assert_eq!(Lazy::force(lazy.clone()), 42);
289 assert_eq!(*counter.borrow(), 1);
290 assert_eq!(Lazy::force(lazy), 42);
291 // Since we clone before forcing, and OnceCell is not shared across clones (it's deep cloned),
292 // the counter increments again.
293 assert_eq!(*counter.borrow(), 2);
294 }
295
296 /// Tests that `Lazy::defer` delays execution until forced.
297 #[test]
298 fn defer_execution_order() {
299 let counter = Rc::new(RefCell::new(0));
300 let counter_clone = counter.clone();
301
302 let lazy = Lazy::<OnceCellBrand, RcFnBrand, _>::defer::<RcFnBrand>(
303 <RcFnBrand as ClonableFn>::new(move |_| {
304 *counter_clone.borrow_mut() += 1;
305 Lazy::new(<RcFnBrand as ClonableFn>::new(|_| 42))
306 }),
307 );
308
309 assert_eq!(*counter.borrow(), 0);
310 assert_eq!(Lazy::force(lazy), 42);
311 assert_eq!(*counter.borrow(), 1);
312 }
313}