Skip to content

Stabilize LazyCell and LazyLock - #121377

Merged
bors merged 1 commit into
rust-lang:masterfrom
pitaj:lazy_cell_fn_pointer
May 26, 2024
Merged

Stabilize LazyCell and LazyLock#121377
bors merged 1 commit into
rust-lang:masterfrom
pitaj:lazy_cell_fn_pointer

Conversation

@pitaj

@pitaj pitaj commented Feb 21, 2024

Copy link
Copy Markdown
Contributor

Closes #109736

This stabilizes the LazyLock and LazyCell types:

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

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Feb 21, 2024
@pitaj
pitaj force-pushed the lazy_cell_fn_pointer branch from 730d4df to bf11b8b Compare February 21, 2024 04:08
@pitaj pitaj added T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. and removed T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Feb 21, 2024
Comment thread library/core/src/cell/lazy.rs Outdated
notgull pushed a commit to rust-windowing/winit that referenced this pull request Feb 25, 2024
Removes the once_cell dependency, instead using std::sync::OnceLock and a
minimal polyfill for std::sync::LazyLock, which may be stabilized soon
(see rust-lang/rust#121377).

This should not require a bump in MSRV, as OnceLock was stabilized in 1.70,
which this crate is using.
@m-ou-se m-ou-se added the I-libs-api-nominated Nominated for discussion during a libs-api team meeting. label Mar 5, 2024
@m-ou-se

m-ou-se commented Mar 5, 2024

Copy link
Copy Markdown
Member

sidestepping the unresolved questions

It is not fully clear to me which issues this sidesteps. Can you add some clarifications of what is and isn't allowed after this PR, and what a possible upgrade path looks like to a future where LazyCell/LazyLock don't unnecessarily store a function pointer in memory?

@m-ou-se m-ou-se added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. I-libs-api-nominated Nominated for discussion during a libs-api team meeting. labels Mar 5, 2024
@pitaj

pitaj commented Mar 5, 2024

Copy link
Copy Markdown
Contributor Author

@m-ou-se

Unresolved Questions

  1. Default F = fn() -> T in type signature (See comment)

Summary: LazyLock<T, F = fn() -> T> is a hack to make static FOO: Lazy<T> work. (It's also not ideal to store a function pointer in memory unnecessarily). Any alternatives would require new language features.

With the unstable default type parameter, specifying the second type parameter is an error. The only way to get a LazyLock<Thing, {closure}> is via type inference on local variables:

// LazyLock<String, {closure}>
let foo = LazyLock::new(|| "foo".to_string());

// LazyLock<String, fn() -> String>
let bar: LazyLock<String> = LazyLock::new(|| "bar".to_string());

// LazyLock<String, fn() -> String>
static STAT: LazyLock<String> = LazyLock::new(|| "stat".to_string());

Some future possibilities that would allow avoiding the function pointer:

// Just "opaque type inference in statics"
pub static LazyLock<T, F = fn() -> T> { ... }
// lint the missing type parameter
static FOO: LazyLock<String> = ...;
// recommend setting it to infer the closure type
static FOO: LazyLock<String, _> = ...;

// "Opaque type inference in statics" and "type parameter default infer"
pub struct LazyLock<T, F = _> { ... }
static FOO: LazyLock<String> = ...;

// "`impl Trait` in statics" and "type parameter default `impl Trait`"
pub struct LazyLock<T, F = impl FnOnce() -> T> { ... }
static FOO: LazyLock<String> = ...;
  1. Is variance of Lazy correct? (See
    Feature request: Make Lazy<T, F> covariant in F matklad/once_cell#167)

Summary: LazyLock<T, F> is currently invariant in F. Using a type including a lifetime for F can cause "insurmountable" lifetime issues. @matklad agrees that covariance would be better, but it would complicate the implementation (or may be impossible). Nobody has implemented F-covariance for once_cell::*::Lazy yet.

Because the unstable type parameter can't be specified explicitly, it's impossible to set it to a type which can cause those variance issues. The only remotely possible way would be type inference in a local variable, but I can't think of a way that could cause issues, if it's possible at all.

There are only two possible futures here:

  • Lazy* are modified to be covariant in F and the parameter is later stabilized
  • The parameter is stabilized as-is, invariant in F

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Mar 5, 2024
@tgross35

tgross35 commented Mar 6, 2024

Copy link
Copy Markdown
Contributor

cc @danielhenrymantilla who did a lot of the variance work for once_cell

@tgross35

tgross35 commented Apr 12, 2024

Copy link
Copy Markdown
Contributor

@m-ou-se is the response in #121377 (comment) sufficient to get this nominated again?

@m-ou-se m-ou-se added the I-libs-api-nominated Nominated for discussion during a libs-api team meeting. label Apr 15, 2024
@pitaj pitaj mentioned this pull request Apr 17, 2024
5 tasks
@rust-log-analyzer

This comment has been minimized.

@pitaj
pitaj force-pushed the lazy_cell_fn_pointer branch from a0c614d to dc75ed4 Compare April 17, 2024 05:28
@Amanieu Amanieu removed the I-libs-api-nominated Nominated for discussion during a libs-api team meeting. label Apr 23, 2024
@Amanieu

Amanieu commented Apr 30, 2024

Copy link
Copy Markdown
Member

We discussed this in a libs-api meeting and decided for a full stabilization of these types as they are right now, including stabilizing the F generic argument.

The reason for also stabilizing F is that even if in the future we get some form of type inference for static, we could just use a lint to suggest using _ to infer F. We expect that if we do get type inference in the future then it is likely that people will end up omitting the type entirely in favor of inference anyways.

Regarding the variance of F, it's not a breaking change to make it co-variant later.

@pitaj

pitaj commented Apr 30, 2024

Copy link
Copy Markdown
Contributor Author

Great news! I will soon modify this PR to fully stabilize, rather than leaving F unstable.

@pitaj
pitaj force-pushed the lazy_cell_fn_pointer branch from dc75ed4 to b4a6715 Compare May 3, 2024 04:24
@rustbot rustbot added the A-translation Area: Translation infrastructure, and migrating existing diagnostics to SessionDiagnostic label May 3, 2024
@rustbot

rustbot commented May 3, 2024

Copy link
Copy Markdown
Collaborator

rustc_error_messages was changed

cc @davidtwco, @compiler-errors, @TaKO8Ki

@RalfJung

Copy link
Copy Markdown
Member

@danielhenrymantilla yeah that's what I would think, similar to how Mutex<()> is sometimes used as a lock where the guarded data (unsafely) lives "outside" the lock.

@RalfJung

Copy link
Copy Markdown
Member

I filed an issue for that: #125615

@slanterns

Copy link
Copy Markdown
Contributor

Actually people have already discussed replacing Once by OnceLock<()> before, but whether it worth the breakage or not remains a question.

@DoubleHyphen

Copy link
Copy Markdown

I tried this code:

use std::sync::LazyLock;
use std::collections::HashMap;

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
});

fn main() {
    println!("{:?}", &HASHMAP);
    HASHMAP.get(&74);
    println!("{:?}", &HASHMAP);
}

And got this output:

LazyLock(<uninit>)
initializing
LazyLock({74: "Hoyten", 13: "Spica"})

Is this the expected behaviour? Is it illegal to attempt to print a static LazyLock before doing anything else with it?

@pitaj

pitaj commented May 28, 2024

Copy link
Copy Markdown
Contributor Author

@DoubleHyphen these Lazy types have explicit Debug impls that will tell you if the object is not initialized. That's what you're seeing with the LazyLock(<uninit>) output. It's expected behavior.

@RalfJung

Copy link
Copy Markdown
Member

@DoubleHyphen what makes you think that anything illegal happened?

@DoubleHyphen

DoubleHyphen commented May 29, 2024

Copy link
Copy Markdown

@DoubleHyphen what makes you think that anything illegal happened?

Terse version: I was expecting printing the LazyLock to constitute using it; thus, I expected it to be printed initialised.


Verbose version:

As I saw this thread, I was immediately reminded of Graydon Hoare's famous article, things rust shipped without. One of those things was

variables with runtime "before main" static initialization

which at first glance seemed at odds with this here feature.

But then I thought “Wait. The clue is in the name. Those variables ought to be initialised before their first use, not before main.“ So I whipped up an example in order to experiment and see when they would be initialised.

Now when it comes to lazy iterators, their uninitialised state is not observable; as soon as one tries to do anything with them, they are immediately initialised. I had expected the same to be true of LazyLocks; thus, I expected printing it to initialise it. It was therefore a bit astonishing to me to realise that this was not the case.

Maybe clear documentation will fix this issue, or at least assuage it. But for now, in its absence, there is no clear way to use a LazyLock to, for instance, get the user's name from the terminal and then print it. How do you persuade the compiler to consider a static LazyLock<String> used, if not by printing it?

I'm not convinced that force is the correct API design choice, but I'll defer to the more experienced folks.

@RalfJung

RalfJung commented May 29, 2024

Copy link
Copy Markdown
Member

How do you persuade the compiler to consider a static LazyLock used, if not by printing it?

I am confused by the question, since your example already does that: you just deref it. Your own code does that when calling get. HASHMAP.get ensures it has been initialized. If you want to debug-print the contents of the LazyCell (rather than debug-printing the LazyCell itself), use println!("{:?}", *HASHMAP);, the * makes the deref explicit that is added implicitly when calling get.

I don't quite understand the problem here. Anyway a merged PR is a bad place for discussion, please open an issue describing the expected and actual behavior, or otherwise clarifying what you are missing in the current API surface.

@elichai

elichai commented May 29, 2024

Copy link
Copy Markdown
Contributor

It's too bad we have to go through a function pointer and not directly through the ZST, is there no way to somehow make inference or <T = impl FnOnce()> (does it even make sense?) something that will allow us to cover us a generic, maybe with TAIT somehow?

EDIT: This was already discussed above

@pitaj

pitaj commented May 29, 2024

Copy link
Copy Markdown
Contributor Author

@elichai I don't think TAIT can help us, since every closure has a unique type. A few possibilities are discussed here, IMO the best option is "type inference in statics". Then the following would just work:

static LAZY_FOO = LazyLock::new(|| "foo".to_uppercase());

@elichai

This comment has been minimized.

@apiraino apiraino removed the to-announce Announce this issue on triage meeting label Jun 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this PR / Issue. relnotes Marks issues that should be documented in the release notes of the next release. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracking Issue for lazy_cell