Implement `race::Lazy` if possible
dhedey opened this issue Β· comments
First, can I say thanks for working on this library! π
Just to quickly explain our use case: we are building code which works primarily in std, but needs to be run occasionally in no-std (WASM, to be exact - but this is only performed as part of an off-line compilation step, so doesn't need to be performant). For this, we ensure our code runs in std
and no-std
(but we have 'alloc').
I wish to create a lazy static look-up of well known types, so I can get a reference into this lookup. Ideally I'd use sync::Lazy
, but that doesn't work in no-std
. Performance at boot-up isn't of concern, so I'm very happy to use race
- but it doesn't support Lazy - meaning either coding my own or change the API to use race::OnceBox
in this case.
Ideally, if it'd be sound, I'd like a race::Lazy
type which is effectively compatible with sync::Lazy
, so we can use either, dependent on if we're in std or no-std. Although I'd also be happy to use race::Lazy
everywhere, if it can't be made compatible. (I guess it would need to take a Fn instead of a FnOnce).
For now, I've swapped back to lazy_static
because the performance concerns of the spin lock don't apply in my use case.
Relatedly, just a suggestion - but the race types are neat and could do with better documentation at the crate root so they're more discoverable https://docs.rs/once_cell/latest/once_cell/index.html
Here's the implementation:
pub struct Lazy<T, F> {
cell: OnceBox<T>,
init: F,
}
impl<T, F> Lazy<T, F> {
pub const fn new(init: F) -> Lazy<T, F>
where
F: Fn() -> T,
{
Lazy { cell: OnceBox::new(), init }
}
}
impl<T, F> ops::Deref for Lazy<T, F>
where
F: Fn() -> T,
{
type Target = T;
fn deref(&self) -> &T {
self.cell.get_or_init(|| Box::new((self.init)()))
}
}
I don't think it makes sense to include this into the crate, as it's just a trivial convenience wrapper. My advice is to use Lazy
only in the simplest of cases, and use OnceCell
directly otherwise.
or change the API to use
I'd advise against exposing once_cell
at the API layer, it generally can, and should be, an implementation detail.
Rather than
pub const GLOBAL_DATA: Lazy<DATA> = once_cell::sync::Lazy::new(...);
prefer
pub fn global_data() -> &'static Data {
...
}
as that reduce the exposed API surface (eg, doing a major update to OnceCell won't be semver breaking).
Thanks @matklad ! I really appreciate your detailed response. You're totally right about the fn being better. (Although the "API" I was talking about was crate internal, your point still stands π )
But just to ask - out of interest, why is there hesitance about adding the above Lazy implementation to the once_cell
crate?
I would imagine this is one of the most common use cases, and if the sync
implementation gets this treatment, why not the no-std-compatible race
implementation?
As you say, the implementation is small, but not having it definitely makes the crate less ergonomic, compared to say, lazy_static
. A reviewer at code review time might quite rightly push back and say "why have you got your own Lazy implementation? Just use lazy_static
". Similarly, a user who doesn't dig through the docs is just going to see "no easy no-std support", spend 5 minutes trying to get it to work, and look elsewhere. If race::Lazy
was marked as a "no-std compatible Lazy" (with caveat it might build multiple times in a race condition) in the crate docs, I imagine it would make once_cell
more appealing to no-std users who just want a tool to get a 'static reference for some cached data.
Just a few cents, but really appreciate your work on this crate, and your willingness to share your knowledge above π
In general, I am not a fan of conveniences: I prefer the code which clearly expresses what actually is happening, rather than the most concise code. So, for me, existence of sync::Lazy
is an exception -- stuffing things into a global static is too overwhelmingly common to not have this (though this absolutely does create a drawback where people get stuck with not finding a particular way to do something with Lazy
, while the correct solution is to use OnceCell
).
In contrast, the race
module is already "off the happy path" --- it is intended for rather narrow circumstances. Adding extra convenience for some fraction of an already uncommon use-case does not seem worthwhile to me.
Similarly, a user who doesn't dig through the docs is just going to see "no easy no-std support", spend 5 minutes trying to get it to work, and look elsewhere.
Yeah, I generally optimize for "looks at the source code of the dependency to make sure it is exactly what they need" users, rather than for "wants to move on with their actual problem as fast as possible". Neither case is inherently more important than the other, and there certainly is a space for once-cell-like create which mostly "just work" in whatever scenario, by using spinlocks, global state, and what not.
Understood. I was sold in a few places that this crate is a natural evolution / more ergonomic than lazy_static
, but it sounds like that's not the intent of this crate. In which case I will advise people/colleagues to continue to use lazy_static
for no-std for now - or point them at this thread (as if I prefer the race
implementation for our use case π).
It might still suggest it would be useful to highlight once-cell's no-std support and the race module in the crate docs so it's more discoverable though :) - https://docs.rs/once_cell/latest/once_cell/index.html
In any case - appreciate your replies! And many thanks for your help.