matklad / once_cell

Rust library for single assignment cells and lazy statics without macros

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.