I think we should broaden possible applications of the LazyCell
type. I have come across this sentiment that LazyCell
is only useful for initializing values at runtime that cannot be evaluated at compile-time. I think this is because of the use of LazyStatic crate, but LazyCell
can potentially be used for a lot more. I think LazyCell
can be used for any arbitrary lazy evaluation. Right now LazyCell
only applies to FnOnce() -> T
. This is a good start and allows for closures in addition to functions, but it has limits. As an example, I want to provide the users of my crate with structs (LotsOfCalcs
) that can calculate material properties using the struct's implemented functions (cheap_prop_calc_X
), but to evaluate these properties I need inputs from the user and I need to calculate intermediate values (expensive_calcs
) that are used to calculate the final properties. I only want the intermediate values to be evaluated if and when the user needs them because they are expensive. The code below tries to illustrate this, the only problem is that LazyCell
does not implement my struct (ExpensiveCalc
), but I think it could with a small change.
struct ExpensiveCalc {
inputs: (f64, f64),
expensive_func: fn(f64, f64) -> f64
}
pub struct LotsOfCalcs {
expensive_calcs: [LazyCell<f64, ExpensiveCalc>; 5]
}
impl LotsOfCalcs {
pub fn cheap_prop_calc_1(&self) -> MadeFromExpensiveCalcs;
pub fn cheap_prop_calc_2(&self) -> MadeFromExpensiveCalcs;
pub fn cheap_prop_calc_3(&self) -> MadeFromExpensiveCalcs;
}
Now there is a way around this so that I could still use LazyCell
as-is, but it requires dynamic dispatch because closures are "un-nameable". I mean you could do it with generics, but you would be juggling around 5 different generics, one for each LazyCell
. I have also heard people suggest using a HashMap
to store cached values instead of LazyCell
s or using OnceCell
somehow, but LazyCell
really is the best fit for this problem. The code below works, but the heap allocation and vtable lookups are going to slow it down and I would like this to be suitable for a hot loop.
pub struct LotsOfCalcs {
expensive_calcs: [LazyCell<f64, Box<dyn FnOnce() -> f64>>; 5]
}
impl LotsOfCalcs {
fn new(inputs: (f64, f64)) -> LotsOfCalcs {
let expensive_calcs = [
LazyCell::new(|| expensive_func_1(inputs)),
...
];
LotsOfCalcs {expensive_calcs}
}
...
}
Instead I want to use the former code, but I need a way for the LazyCell
to support the ExpensiveCalc
type. I propose a simple CallOnce
public trait in the std::cell
module, see below. This would be the same as the FnOnce() -> T
trait, except it could be implemented by structs or enums. Instead of LazyCell
being implemented for <T: FnOnce() -> T>
it would be implemented for <T: CallOnce>
.
pub trait CallOnce{
type Output;
fn call_once(self) -> Self::Output;
}
I think it would be trivial to implement this trait for all functions and closures, see below.
impl<T, F: FnOnce() -> T> CallOnce for F {
type Output = T;
fn call_once(self) -> Self::Output {
self()
}
}
Similarly, CallOnce
could be implemented for my ExpensiveCalc
as follows:
impl CallOnce for ExpensiveCalc {
type Output = f64;
fn call_once(self) -> Self::Output {
(self.expensive_func)(self.inputs.0, self.inputs.1)
}
}
The final option would be to make a bespoke type for my application; however, it would literally be an exact copy for LazyCell
with the aforementioned addition.