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 · 47 comments · Fixed by #121377
Closed
5 tasks done

Tracking Issue for lazy_cell #109736

tgross35 opened this issue Mar 29, 2023 · 47 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

Note: lazy_cell_consume is now tracked at #125623

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
Jeremiah-Griffin added a commit to Jeremiah-Griffin/TIFF_tags that referenced this issue Apr 18, 2023
@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.

@joboet
Copy link
Member

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
Member

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
@workingjubilee
Copy link
Member

We need a lazy_cell_consume tracking issue.

xyyx pushed a commit to nitrogen-project/android_system_core that referenced this issue Jun 13, 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

Former-commit-id: 794a952642317f7da1be2ea69e6865c55ea37be6
micky387 pushed a commit to omnirom/android_system_core_old that referenced this issue Jun 16, 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
shivaraj-bh added a commit to juspay/omnix that referenced this issue Sep 6, 2024
This is to bypass:

error[E0658]: use of unstable library feature 'lazy_cell'
 --> crates/omnix-init/src/config.rs:1:48
  |
1 | use std::{collections::HashMap, path::PathBuf, sync::LazyLock};
  |                                                ^^^^^^^^^^^^^^
  |
  = note: see issue #109736
<rust-lang/rust#109736> for more information
  = help: add `#![feature(lazy_cell)]` to the crate attributes to enable
  = note: this compiler was built on 2024-05-11; consider upgrading it
if it is out of date
twwn added a commit to twwn/anki that referenced this issue Sep 27, 2024
…s possible

Since 1.80: rust-lang/rust#109736 and rust-lang/rust#98165

Non-Thread-Safe Lazy → std::cell::LazyCell https://doc.rust-lang.org/nightly/std/cell/struct.LazyCell.html

Thread-safe SyncLazy → std::sync::LazyLock https://doc.rust-lang.org/nightly/std/sync/struct.LazyLock.html

The compiler accepted LazyCell only in minilints.

The final use in rslib/src/log.rs couldn't be replaced since get_or_try_init has not yet been standardized: rust-lang/rust#109737
twwn added a commit to twwn/anki that referenced this issue Sep 27, 2024
…s possible

Since 1.80: rust-lang/rust#109736 and rust-lang/rust#98165

Non-Thread-Safe Lazy → std::cell::LazyCell https://doc.rust-lang.org/nightly/std/cell/struct.LazyCell.html

Thread-safe SyncLazy → std::sync::LazyLock https://doc.rust-lang.org/nightly/std/sync/struct.LazyLock.html

The compiler accepted LazyCell only in minilints.

The final use in rslib/src/log.rs couldn't be replaced since get_or_try_init has not yet been standardized: rust-lang/rust#109737
twwn added a commit to twwn/anki that referenced this issue Sep 27, 2024
… possible

Since 1.80: rust-lang/rust#109736 and rust-lang/rust#98165

Non-Thread-Safe Lazy → std::cell::LazyCell https://doc.rust-lang.org/nightly/std/cell/struct.LazyCell.html

Thread-safe SyncLazy → std::sync::LazyLock https://doc.rust-lang.org/nightly/std/sync/struct.LazyLock.html

The compiler accepted LazyCell only in minilints.

The final use in rslib/src/log.rs couldn't be replaced since get_or_try_init has not yet been standardized: rust-lang/rust#109737
twwn added a commit to twwn/anki that referenced this issue Sep 27, 2024
… possible

Since 1.80: rust-lang/rust#109736 and rust-lang/rust#98165

Non-Thread-Safe Lazy → std::cell::LazyCell https://doc.rust-lang.org/nightly/std/cell/struct.LazyCell.html

Thread-safe SyncLazy → std::sync::LazyLock https://doc.rust-lang.org/nightly/std/sync/struct.LazyLock.html

The compiler accepted LazyCell only in minilints.

The final use in rslib/src/log.rs couldn't be replaced since get_or_try_init has not yet been standardized: rust-lang/rust#109737
dae pushed a commit to ankitects/anki that referenced this issue Sep 30, 2024
* Anki: Replace lazy_static with once_cell

Unify to once_cell, lazy_static's replacement. The latter in unmaintained.

* Anki: Replace once_cell with stabilized LazyCell / LazyLock as far as possible

Since 1.80: rust-lang/rust#109736 and rust-lang/rust#98165

Non-Thread-Safe Lazy → std::cell::LazyCell https://doc.rust-lang.org/nightly/std/cell/struct.LazyCell.html

Thread-safe SyncLazy → std::sync::LazyLock https://doc.rust-lang.org/nightly/std/sync/struct.LazyLock.html

The compiler accepted LazyCell only in minilints.

The final use in rslib/src/log.rs couldn't be replaced since get_or_try_init has not yet been standardized: rust-lang/rust#109737

* Declare correct MSRV (dae)

Some of our deps require newer Rust versions, so this was misleading.

Updating the MSRV also allows us to use .inspect() on Option now
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.