Generalising Lazy::force to non-functional types
01mf02 opened this issue · comments
I need to define a function that constructs a lazy value based on some input, such as follows:
fn upper_lazy1(s: String) -> Lazy<String> {
Lazy::new(|| s.to_uppercase())
}
However, the compiler complains that it "expected fn pointer, found closure".
Good. So I could return Lazy<String, impl FnOnce() -> String>
instead, but this makes it difficult to store this lazy value in a struct (due to the impl
). Or I could box the closure, but then I get unwanted overhead, and I have to wrestle with lifetimes.
It would be nice if I could write something like:
struct Uppercase(String);
impl Into<String> for Uppercase {
fn into(self) -> String {
self.0.to_uppercase()
}
}
fn upper_lazy(s: String) -> Lazy<String, Uppercase> {
Lazy::new(Uppercase(s))
}
fn main() {
let lazy = upper_lazy("Hello, world!".to_string());
// the next line does not work
assert_eq!(Lazy::force(&lazy), "HELLO, WORLD!");
}
However, the current definition of the Lazy::force
function does not permit this, because it demands that
in Lazy<T, F>
, F
implements FnOnce() -> T
.
So I see a few possible alternative bounds for F
in Lazy::force
to support my use case:
FnOnce() -> T
:
This is the current solution chosen byLazy
. This works only for initialising lazy values with functions/closures AFAIK.
It might support my use case eventually if I could implementFnOnce
for custom types such asUppercase
, but
this is currently unstable and has been discussed since more than five years ...
rust-lang/rust#29625Into<T>
:
This supports my use case perfectly.
However, it excludesFnOnce
, becauseInto<T>
is not implemented forFnOnce() -> T
.
(Is there any chance ofInto<T>
getting implemented forFnOnce() -> T
one day?)- A new trait, called e.g.
Evaluate<T>
:
This trait has the same signature asInto<T>
, but because it is a new trait,
it can be implemented forFnOnce() -> T
as well as for custom types such asUppercase
.
That's what I am doing right now, but this might be ugly in case that one of the two previous options becomes viable eventually.
The Evaluate<T>
approach can be implemented as follows:
pub trait Evaluate<T> {
fn evaluate(self) -> T;
}
impl<T, F: FnOnce() -> T> Evaluate<T> for F {
fn evaluate(self) -> T {
self()
}
}
// adapted from current code in `once_cell`
impl<T, F: Evaluate<T>> Lazy<T, F> {
pub fn force(this: &Lazy<T, F>) -> &T {
this.cell.get_or_init(|| match this.init.take() {
Some(f) => f.evaluate(),
None => panic!("Lazy instance has previously been poisoned"),
})
}
}
What do you think about generalising Lazy
to support use cases like mine?
Lazy is just a thin convenience layer over OnceCell. If lazy doesn’t work for your particular use case, you can build arbitrary abstractions on top of OnceCell.
I know. However, it would be a pity to restrict Lazy
to types implementing FnOnce
if there was a more general way ...
I thought of another way than modifying the trait bounds of force
: Introduce a new function, e.g. force_with
, which takes a function that evaluates the initial value given to Lazy::new
.
impl<T, F> Lazy<T, F> {
pub fn force_with<G: FnOnce(F) -> T>(&self, g: G) -> &T {
self.cell.get_or_init(|| match self.init.take() {
Some(f) => g(f),
None => panic!("Lazy instance has previously been poisoned"),
})
}
}
impl<T, F: FnOnce() -> T> Lazy<T, F> {
pub fn force(&self) -> &T {
self.force_with(|f| f())
}
}
Example usage:
fn main() {
let lazy = Lazy::new("Hello, world!".to_string());
assert_eq!(
Lazy::force_with(&lazy, |s| s.to_uppercase()),
"HELLO, WORLD!"
);
}
This approach is strictly more permissive than the current one in Lazy
and is backwards-compatible.
Furthermore, this allows the lazy value to be evaluated with different functions, depending on where it is evaluated.
However, it would be a pity to restrict Lazy to types implementing FnOnce if there was a more general way ...
Maximal generality is a non-goal for once_cell. Rather, we strive for the simplest API which can serve as building block for most of use-cases.
What you are describing is I think a LazyTransform. It is indeed somewhat more general that OnceCell (not strictly more general, as it requires poisoning), but it is a rather niche thing, and can be implemented on top of once cell:
pub struct OnceTransform<U, T> {
u: Cell<Option<U>>,
t: OnceCell<T>,
}
impl<U, T> OnceTransform<U, T> {
pub fn get_or_init(&self, f: impl FnOnce(U) -> T) -> &T {
self.t.get_or_init(|| {
let u = self.u.take().expect("poisoned");
f(u)
})
}
}
So, provide it here doesn't fit the design goals of once_cell
That being said, the API of OnceCell has been ironed out quite a bit, and is on path to inclusion in std, so I think we can tolerate a bit of a scope creep now. OnceCell-based impl of OnceTransform would suffer from two drawbacks:
- a smudge of (tricky) unsafe is needed to make it
Sync
- The repr would store an
(Option<T>, Option<U>)
rather than anOption<Either<T, U>>
.
Adding a fresh type for that under an unstable
flag would be OK.
It seems that this issue will be resolved when the either fn_traits
or type_alias_impl_trait
feature is stable.
Hm, I see. Well, then I think that my proposition does not make so much sense.
Thanks for taking your time to discuss this with me! :)