Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tracking Issue for lazy_cell #109736

Closed
5 tasks done
tgross35 opened this issue Mar 29, 2023 · 46 comments · Fixed by #121377
Closed
5 tasks done

Tracking Issue for lazy_cell #109736

tgross35 opened this issue Mar 29, 2023 · 46 comments · Fixed by #121377
Labels
C-tracking-issue Category: A tracking issue for an RFC or an unstable feature. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@tgross35
Copy link
Contributor

tgross35 commented Mar 29, 2023

This supercedes #74465 after a portion of once_cell was stabilized with #105587

Feature gate: #![feature(lazy_cell)]

This is a tracking issue for the LazyCell and LazyLock types, which are designed to support convenient one-time initialization. One of the main goals is to be able to replace the lazy_static crate, as well as once_cell::{sync, unsync}::Lazy.

Public API

// core::cell (in core/src/cell/lazy.rs)

pub struct LazyCell<T, F = fn() -> T> { /* ... */ }

impl<T, F: FnOnce() -> T> LazyCell<T, F> {
    pub const fn new(init: F) -> LazyCell<T, F>;
    pub fn force(this: &LazyCell<T, F>) -> &T;
}

impl<T, F: FnOnce() -> T> Deref for LazyCell<T, F> {
    type Target = T;
}

impl<T: Default> Default for LazyCell<T>;
impl<T: fmt::Debug, F> fmt::Debug for LazyCell<T, F>;
// std::sync (in std/sync/lazy_lock.rs)

pub struct LazyLock<T, F = fn() -> T> { /* ... */ }

impl<T, F: FnOnce() -> T> LazyLock<T, F> {
    pub const fn new(f: F) -> LazyLock<T, F>;
    pub fn force(this: &LazyLock<T, F>) -> &T;
}

impl<T, F> Drop for LazyLock<T, F>;
impl<T, F: FnOnce() -> T> Deref for LazyLock<T, F> {
    type Target = T;
}
impl<T: Default> Default for LazyLock<T>;
impl<T: fmt::Debug, F> fmt::Debug for LazyLock<T, F>;

// We never create a `&F` from a `&LazyLock<T, F>` so it is fine
// to not impl `Sync` for `F`
unsafe impl<T: Sync + Send, F: Send> Sync for LazyLock<T, F>;
// auto-derived `Send` impl is OK.
impl<T: RefUnwindSafe + UnwindSafe, F: UnwindSafe> RefUnwindSafe for LazyLock<T, F>;
impl<T: UnwindSafe, F: UnwindSafe> UnwindSafe for LazyLock<T, F>;

Steps / History

Unresolved Questions

Footnotes

  1. https://std-dev-guide.rust-lang.org/feature-lifecycle/stabilization.html

@tgross35 tgross35 added C-tracking-issue Category: A tracking issue for an RFC or an unstable feature. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels Mar 29, 2023
@tgross35
Copy link
Contributor Author

cc original authors @matklad and @KodrAus, just for reference

@matklad
Copy link
Member

matklad commented Mar 29, 2023

The biggest design question here is the default parameter: F = fn() -> T

  • It is a hack to make static MY_DATA: Lazy<MyType> = ... syntax work.
  • One can imagine static MY_DATA: Lazy<MyType, _> working one day, but at this point it seems more likely than not that we won't ever implement this kind of inference, and, even if we end up implementing something like that, it would be years in the future.
  • The hack works out nicely in 99% of the cases, but it can create confusion when using Lazy for a local variable:
let env = "hello".to_string();

let ok1 = Lazy::new(|| env);
let ok2: Lazy<String, _> = Lazy::new(|| env);

let err: Lazy<String> = Lazy::new(|| env);
// ^ The confusing case. The problem here is that type of `F` isn't inferred, 
/// but is taken from the default

@tgross35
Copy link
Contributor Author

It is kind of weird to have the F be a part of the type definition at all. It makes sense of course as far as needing to store the function type in the struct, but it's a bit clunky for type signatures.

Is there a workaround using with raw function pointers maybe? Since the signature is always known, but I'm not sure how things like closure coercions & lifetimes would work here.

Or maybe dynamics could work. I haven't thought it totally through (and there might be lifetime trickiness) but at least the std LazyLock could store a Box<dyn FnOnce() -> T>

@Sp00ph
Copy link
Member

Sp00ph commented Mar 30, 2023

Boxing the contents would make it unusable for static values which would render it useless for most use cases.

@tgross35
Copy link
Contributor Author

tgross35 commented Mar 30, 2023

Yeah, that is a good point. And &dyn isn't very elegant.

Just spitballing here... taking a fn() -> T would eliminate the second parameter. This would unfortunately mean that it can't take environment-capturing closures, blocking the examples matklad pointed out. At least for LazyLock though, I can't really envision any use cases where this would be desired anyway. And it would allow for a nonbreaking upgrade to FnOnce in the future, if there is ever a better way.

pub struct TestLazyLock<T> {
    cell: OnceLock<T>,
    init: Cell<Option<fn() -> T>>,
}

impl<T> TestLazyLock<T> {
    const fn new(init: fn() -> T) -> Self {
        Self { cell: OnceLock::new(), init: Cell::new(Some(init)) }
    }
}

playground

@Sp00ph
Copy link
Member

Sp00ph commented Mar 30, 2023

I don't see how that would be preferable over just having the second type parameter with the fn() -> T default. Granted, the case mentioned by @matklad currently doesn't provide a very good diagnostic, but if the compiler just suggested to add the _ as the second type parameter then that point of confusion would probably also largely disappear.

@SUPERCILEX
Copy link
Contributor

Regarding #106152, does anyone know if the initialization state was explicitly excluded from the API? That was @Amanieu's concern.

@tgross35
Copy link
Contributor Author

Yeah, it's not preferable. Just trying to see if there's any way where we could either

  1. not have that generic parameter, or
  2. make it so users can never write LazyCell<T, _> or LazyCell<T, fn() -> T> - so we could eventually drop the second parameter in a background-compatible way. I don't think this is possible via sealing or anything, but maybe there's a tricky way.

To quote @m-ou-se in #74465 (comment)

It's a bit of a shame that Lazy uses a fn() -> T by default. With that type, it needlessly stores a function pointer even if it is constant. Would it require big language changes to make it work without storing a function pointer (so, a closure as ZST), while still being as easy to use? Maybe if captureless closures would implement some kind of const Default? And some way to not have to name the full type in statics. That's probably not going to happen very soon, but it'd be a shame if this becomes possible and we can't improve Lazy because the fn() -> T version was already stabilized. Is there another way to do this?

I think that the form I suggested above with TestLazyLock<T> would be forward-compatible with either something like what Mara is suggesting, or with the current form (could use a sanity check here). It's not as useful as the current full featured version, but it does directly replace the purpose of lazy_static, which is kind of the biggest target of this feature. So in theory, that could be stabilized while a more full featured version is being contemplated.

@tgross35
Copy link
Contributor Author

Regarding #106152, does anyone know if the initialization state was explicitly excluded from the API? That was @Amanieu's concern.

I am not super in the know for this, but I don't think there's any particular reason the state couldn't be exposed somehow. The state is known by the OnceCell, and would have to be tracked somehow even with a different underlying implementation.

@bstrie
Copy link
Contributor

bstrie commented Mar 30, 2023

make it so users can never write LazyCell<T, _> or LazyCell<T, fn() -> T> - so we could eventually drop the second parameter in a background-compatible way

I somewhat doubt it would ever be necessary to make such a change (the only edge case is rather contrived), but even then the way to drop the second parameter in a backward-compatible way would be to introduce a new API, deprecate this one, and upgrade everyone with cargo fix. I wouldn't stress about the existence of this parameter.

@NobodyXu
Copy link
Contributor

NobodyXu commented Mar 31, 2023

If "impl trait" is allowed in let/static/const #63065 then the additional parameter is not an issue anymore.

If the Lazy is initialized with a closure, then using "impl trait" in static would actually reduce size of the global variable by one function pointer.

@tgross35
Copy link
Contributor Author

tgross35 commented Mar 31, 2023

@NobodyXu I'm not too familar with that feature... would it also allow these impl definitions in structs? Or what would this look like? I'm imagining something like this, which would be quite cool

type LazyInitFn<T> =  impl FnOnce() -> T + Send + ?Sized;

pub struct LazyLock<T> {
    cell: OnceLock<T>,
    init: Cell<Option<LazyInitFn<T>>>,
}

impl<T> LazyLock<T> {
    const fn new(init: LazyInitFn<T>) -> Self { /* ... */ }
}

But I haven't seen any examples in the RFC that do this

@NobodyXu
Copy link
Contributor

@NobodyXu I'm not too familar with that feature... would it also allow these impl definitions in structs? Or what would this look like? I'm imagining something like this, which would be quite cool

Oh you are right, I missed that.

According to my understanding, it enables something like this:

static F: impl FnOnce() -> u32 = || 1;

I was thinking about:

static F: Lazy<impl FnOnce() -> u32> = Lazy::new(|| 1);

Which might not be covered by the tracking issue I linked.

@matklad
Copy link
Member

matklad commented Apr 2, 2023

Added matklad/once_cell#167 as an unresolved quesrion

@tgross35
Copy link
Contributor Author

tgross35 commented Apr 3, 2023

Is there a reason force is an associated function rather than taking &self? No specific comment, just curious.

@bjorn3
Copy link
Member

bjorn3 commented Apr 3, 2023

I presume to avoid shadowing a .force() method on the inner value.

@tgross35
Copy link
Contributor Author

tgross35 commented Apr 3, 2023

Makes sense. It does sort of remind me of the discussion on Mutex::unlock, but I do think it makes more sense here.

@slanterns
Copy link
Contributor

Unlike Mutex, LazyCell is newly added to std. Why will it still suffer from the possible shadowing?

@tgross35
Copy link
Contributor Author

tgross35 commented Apr 3, 2023

Bjorn meant that if whatever the LazyCell derefs to (T) has a .force method, then that would overlap with the LazyCells's own .force.

I just linked Mutex::unlock because it was recently decided against for just being a synonym for drop, and it has a similar signature to LazyCell::force - which is a synonym for deref. Just something to consider whether any arguments against unlock might apply here: I don't think they do, since force isn't trying to encourage a way around any usage patterns (unlock was sort of an escape hatch for RAII).

@nbdd0121
Copy link
Contributor

nbdd0121 commented Apr 6, 2023

This works on nightly, if TAIT is enabled:

type F = impl FnOnce() -> ();
static L: LazyLock<(), F> = LazyLock::new(|| ());

remi-dupre added a commit to remi-dupre/gateway that referenced this issue Apr 13, 2023
once_cell supersets lazy_static features and its API is better and
"soon" to be stabilized in the standard library:
rust-lang/rust#109736
remi-dupre added a commit to remi-dupre/gateway that referenced this issue Apr 14, 2023
once_cell supersets lazy_static features and its API is better and
"soon" to be stabilized in the standard library:
rust-lang/rust#109736
@SUPERCILEX
Copy link
Contributor

The lazy_cell_consume feature was added as part of this tracking issue: #106152. It is part of the Lazy* API, but does not have to be stabilized with the base feature set.

minaripenguin pushed a commit to minaripenguin/android_system_core that referenced this issue Mar 4, 2024
Using lazy_static is now discouraged as unmaintained
and once_cell is the recommended replacement.

On top of that a similar implementation found in
once_cell is being tracked for inclusion under the
`lazy_cell` feature gate [0]

[0] rust-lang/rust#109736

Test: m
Change-Id: I21d343a38dbd25bb2d13f239f7fa3a2d7f20323e
minaripenguin pushed a commit to minaripenguin/android_system_core that referenced this issue Mar 4, 2024
Using lazy_static is now discouraged as unmaintained
and once_cell is the recommended replacement.

On top of that a similar implementation found in
once_cell is being tracked for inclusion under the
`lazy_cell` feature gate [0]

[0] rust-lang/rust#109736

Test: m
Change-Id: I21d343a38dbd25bb2d13f239f7fa3a2d7f20323e
@vlovich
Copy link

vlovich commented Mar 20, 2024

I know I'm kinda late to the party, but needed something like LazyLock and realized that even if stabilized the API wouldn't work for me. The reason is that I don't have the callback function handy when the LazyLock is constructed.

Would it be reasonable to instead change the API to .read(f: FnOnce() -> T) instead of using Deref so that everything can be provided lazily and initializing LazyLock is super fast no matter how complex the closure? It would also change the size of the type from max(std::mem::size_of<FnOnce() -> T>(), std::mem::size_of<T>()) to std::mem::size_of<Option<T>>. Probably smaller for any non-trivial kind of closure & no need to copy the closure around when constructing LazyCell/LazyLock. These APIs could then also reasonably also implement Clone when T: Clone).

@NobodyXu
Copy link
Contributor

Would it be reasonable to instead change the API to .read(f: FnOnce() -> T) instead of using Deref so that everything can be provided lazily and initializing LazyLock is super fast no matter how complex the closure? It would also change the size of the type from

I think you can use OnceLock for this?

@vlovich
Copy link

vlovich commented Mar 20, 2024

That's a very good point. Not sure why I thought it wasn't what I wanted.

@workingjubilee
Copy link
Contributor

workingjubilee commented Mar 21, 2024

This API feels a bit off because I would like to have the option to have a LazyLock that doesn't require blocking if 99% of the things interacting with that LazyLock want the blocking version but one wants to just take a quick peek to see if it's already init.

@pitaj
Copy link
Contributor

pitaj commented Mar 21, 2024

I'm not sure what you mean. Can you give an example? Maybe something like this?

/// Returns `Some(&value)` if initialized,
/// Otherwise returns `None`
fn try_get(this: &LazyLock<T, F>) -> Option<&T>

Probably something we can add after stabilizing the current API.

@rben01
Copy link

rben01 commented Apr 11, 2024

I don't know where the best place to note this is so I'll just add it here: the error message for using non-const functions in static declarations currently points users to the once_cell crate (and I don't see any changes to this message in any of the PRs I found related to this issue; I might've just missed them though). As part of lazy_cell stabilization, this error message should be updated to point to one or both of LazyCell and LazyLock.

err.note("consider wrapping this expression in `Lazy::new(|| ...)` from the `once_cell` crate: https://crates.io/crates/once_cell");

@pitaj
Copy link
Contributor

pitaj commented Apr 17, 2024

@rben01 thanks for pointing that it, it's now implemented in #121377

jieyouxu added a commit to jieyouxu/rust that referenced this issue Apr 29, 2024
…i-obk

Remove direct dependencies on lazy_static, once_cell and byteorder

The relevant functionality of all three crates is now available and stable in the standard library, i.e. `std::sync::OnceLock` and `{integer}::to_le_bytes`. I think waiting for `LazyLock` (rust-lang#109736) would give marginally more concise code, but not by much.
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Apr 29, 2024
Rollup merge of rust-lang#124475 - GKFX:more-dependency-pruning, r=oli-obk

Remove direct dependencies on lazy_static, once_cell and byteorder

The relevant functionality of all three crates is now available and stable in the standard library, i.e. `std::sync::OnceLock` and `{integer}::to_le_bytes`. I think waiting for `LazyLock` (rust-lang#109736) would give marginally more concise code, but not by much.
@joboet
Copy link
Contributor

joboet commented May 4, 2024

Shouldn't LazyCell/LazyLock be also covariant over T? From a type system perspective, &LazyCell<T, F> is basically just a &(T, F) which dynamically chooses between calling F or returning a reference to T. This would e.g. allow using a &LazyCell<&'static i32, fn() -> &'static i32> where a &LazyCell<&'a i32, fn() -> &'a i32> was expected.

This might be very difficult to achieve, but if the tree borrows model for interior mutability ends up being used, I can see a way (by adding a Cell<()> field, we can remove the UnsafeCell around the state and still legally mutate it).

@Plecra
Copy link

Plecra commented May 4, 2024

No, I don't think that's right - look at it more like a &(&mut T, Fn() -> T) - we need to be able to store the constructed T after calling the init function, so it mustnt return a supertype of T. We'd need to be able to do something clever with decoupling the return type of init from T.

// concretely,
let captured = 10u8;
let cell = LazyCell::new(|| &captured);
fn init_it<'a>(v: &'a (), cell: &'a LazyCell<&'a u8, impl FnOnce() -> &'a u8>) {
  &**cell; // call the function and store a `&'a u8` as the value
}
init_it(&(), &cell); // 'a is dead
let v: &u8 = &*a; // and when we access it, we've read it in 'b where not `'a: 'b`

In terms of your example, the &LazyCell<&'a i32, fn() -> &'a i32> would now be writing a &'a i32 to the target LazyCell<&'static i32, fn() -> &'static i32>, which is invalid.

(edit: oh and I should add that this doesn't create any problems w.r.t. Treed borrows itself afaik - the tagging is all still correct, but breaking the subtyping relationship on the lifetimes does create problems in the verification of borrowck as far as I understand it. someone might know better than me there 😁)

edit2: after discussing this on discord we realized that type erasing the function pointer on construction does the trick for "something clever with decoupling the return type of init from T". Neat, but obviously undesirable for the actual implementation

fn init_it<F: FnOnce() -> T>(f: *mut F, out: *mut T) {
    *out = (*f)()
}
fn LazyCell::new(f: impl FnOnce() -> T) {
    Uninit(f, init_it::<F> as fn(*mut (), *mut ()))
}
fn LazyCell::force() -> &T {
    (self.init_fn)(&mut self.f as *mut _ as *mut _, self.cell.as_mut_ptr() as *mut _ as *mut _)
    &self.cell
}

Mrick343 pushed a commit to euclidTeam/system_core that referenced this issue May 8, 2024
Using lazy_static is now discouraged as unmaintained
and once_cell is the recommended replacement.

On top of that a similar implementation found in
once_cell is being tracked for inclusion under the
`lazy_cell` feature gate [0]

[0] rust-lang/rust#109736

Change-Id: I21d343a38dbd25bb2d13f239f7fa3a2d7f20323e
@pitaj
Copy link
Contributor

pitaj commented May 21, 2024

For anyone following, this is your notice that #121377 (which will fully stabilize LazyCell and LazyLock as is) has entered the Final Comment Period.

@tgross35
Copy link
Contributor Author

Shouldn't LazyCell/LazyLock be also covariant over T? From a type system perspective, &LazyCell<T, F> is basically just a &(T, F) which dynamically chooses between calling F or returning a reference to T. This would e.g. allow using a &LazyCell<&'static i32, fn() -> &'static i32> where a &LazyCell<&'a i32, fn() -> &'a i32> was expected.

@joboet it was mentioned at #121377 (comment) that making the types covariant over F could be a future nonbreaking change. Is this also true over T?

caspermeijn added a commit to caspermeijn/prost that referenced this issue May 22, 2024
Open question:
- Do we want to wait for LazyLock? rust-lang/rust#109736
@joboet
Copy link
Contributor

joboet commented May 23, 2024

Shouldn't LazyCell/LazyLock be also covariant over T? From a type system perspective, &LazyCell<T, F> is basically just a &(T, F) which dynamically chooses between calling F or returning a reference to T. This would e.g. allow using a &LazyCell<&'static i32, fn() -> &'static i32> where a &LazyCell<&'a i32, fn() -> &'a i32> was expected.

@joboet it was mentioned at #121377 (comment) that making the types covariant over F could be a future nonbreaking change. Is this also true over T?

I don't understand why making it covariant over F would not be breaking; but I imagine the same rationale would apply for T...

caspermeijn added a commit to caspermeijn/prost that referenced this issue May 24, 2024
Open question:
- Do we want to wait for LazyLock? rust-lang/rust#109736
@bors bors closed this as completed in 890982e May 26, 2024
rust-timer added a commit to rust-lang-ci/rust that referenced this issue May 26, 2024
Rollup merge of rust-lang#121377 - pitaj:lazy_cell_fn_pointer, r=dtolnay

Stabilize `LazyCell` and `LazyLock`

Closes rust-lang#109736

This stabilizes the [`LazyLock`](https://doc.rust-lang.org/stable/std/sync/struct.LazyLock.html) and [`LazyCell`](https://doc.rust-lang.org/stable/std/cell/struct.LazyCell.html) types:

```rust
static HASHMAP: LazyLock<HashMap<i32, String>> = LazyLock::new(|| {
    println!("initializing");
    let mut m = HashMap::new();
    m.insert(13, "Spica".to_string());
    m.insert(74, "Hoyten".to_string());
    m
});

let lazy: LazyCell<i32> = LazyCell::new(|| {
    println!("initializing");
    92
});
```

r? libs-api
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-tracking-issue Category: A tracking issue for an RFC or an unstable feature. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.