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

standard lazy types #2788

Merged
merged 15 commits into from
Mar 30, 2023
Merged

standard lazy types #2788

merged 15 commits into from
Mar 30, 2023

Conversation

matklad
Copy link
Member

@matklad matklad commented Oct 18, 2019

Add support for lazy initialized values to standard library, effectively superseding the popular lazy_static crate.

use std::sync::Lazy;

// `BACKTRACE` implements `Deref<Target = Option<String>>` 
// and is initialized on the first access
static BACKTRACE: Lazy<Option<String>> = Lazy::new(|| {
    std::env::var("RUST_BACKTRACE").ok()
});

Rendered

@matklad matklad added T-libs-api Relevant to the library API team, which will review and decide on the RFC. A-types-libstd Proposals & ideas introducing new types to the standard library. labels Oct 18, 2019
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
@Centril Centril added A-cell Proposals relating to interior mutability. A-sync Synchronization related proposals & ideas labels Oct 18, 2019
Co-Authored-By: Mazdak Farrokhzad <twingoow@gmail.com>
@pitdicker
Copy link
Contributor

Great RFC, I hope it makes it into the standard library!

One other aspect that the crate conquer_once tackles is a distinction between blocking and non-blocking methods. I believe this is one possible way to divide the api?

    /// Never blocks, returns `None` if uninitialized, or while another thread is initializing the `OnceCell` concurrently.
    pub fn get(&self) -> Option<&T>;

    /// Never blocks, returns `Err` if initialized, or while another thread is initializing the `OnceCell` concurrently.
    pub fn set(&self, value: T) -> Result<(), T>;

    /// Blocks if another thread is initializing the `OnceCell` concurrently.
    pub fn get_or_init<F>(&self, f: F) -> &T
    where
        F: FnOnce() -> T,
    ;

    /// Blocks if another thread is initializing the `OnceCell` concurrently.
    pub fn get_or_try_init<F, E>(&self, f: F) -> Result<&T, E>
	where
        F: FnOnce() -> Result<T, E>,
    ;
}

Alternatively, all four methods could be blocking, and a try_get and try_set may suffice for a non-blocking use case.

But don't let this comment derail the discussion about whether to include OnceCell in the standard library to much.

@SimonSapin
Copy link
Contributor

fn pointers are not ZSTs, so we waste one pointer per static lazy value. Lazy locals will generally rely on type-inference and will use more specific closure type.

It looks like rust-lang/rust#63065 will allow using a zero-size closure type in a static:

static FOO: Lazy<Foo, impl FnOnce() -> Foo> = Lazy::new(|| foo());

But this will require spelling the item type twice. Maybe that repetition could be avoided… with a macro… that could be named lazy_static! :)

@RustyYato
Copy link

RustyYato commented Oct 18, 2019

@SimonSapin or we can add bounds to Lazy like so

struct Lazy<F: FnOnce<()>> { .. }

and use it like this,

static FOO: Lazy<impl FnOnce() -> Foo> = Lazy(foo);

@SimonSapin
Copy link
Contributor

@KrishnaSannasi How does that help with the requirement that static items name their type without inference?

static FOO: &[_] = &[2, 4];

(Playground)

   Compiling playground v0.0.1 (/playground)
error[E0121]: the type placeholder `_` is not allowed within types on item signatures
 --> src/lib.rs:1:15
  |
1 | static FOO: &[_] = &[2, 4];
  |               ^ not allowed in type signatures

@RustyYato
Copy link

RustyYato commented Oct 19, 2019

@SimonSapin I'm not sure what you are asking, I didn't use _ anywhere in my example. It would be nice if we could have type inference in static/const that only depended on their declaration, but we don't have that (yet).

You proposed using impl Trait to get rid of the cost of fn() -> ..., and noted a paper cut where you would have to name a type multiple times, and I proposed a way to get rid of that paper cut by changing Lazy by removing the redundant type parameter.

@sfackler
Copy link
Member

It doesn't really seem all that worth it to me to spend time worrying about how to inline a function that is called at most one time in the entire execution of a program.

@pitdicker
Copy link
Contributor

A small difference that may be worth noting: std::sync::Once will only run its closure once, while OnceCell::get_or{_try}_init will one run its closure once successfully.

@matklad
Copy link
Member Author

matklad commented Oct 21, 2019

@pitdicker pointed out that we actually can have some part of the sync API exposed via core. This unfortunately increases design/decision space a bit :) I've amended the rfc.

@tarcieri
Copy link

@jhpratt the author of this RFC @matklad is the author of once_cell. It also mentions:

The proposed API is directly copied from once_cell crate.

Altogether, this RFC proposes to add four types:

* `std::cell::OnceCell`, `std::cell::Lazy`
* `std::sync::OnceCell`, `std::sync::Lazy`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that we have sync::Once and cell::Cell, but this may be confusing. Just a thought.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kinda like the combination of both, so the naming may be good (modulo Send/Sync concerns). But it would be better if the !Sync version had a different name from the Sync version.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps acknowledging OnceCell as a WORM (write once, read many) might make WormCell be a better name? Assuming that the implementation isn't too sluggish.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also would really like to see there be a different name for these, rather than relying on the fact that they lie in two separate modules. Maybe just prefixing the sync implementations with Sync is enough, i.e. SyncLazy and Lazy.

Copy link
Member Author

@matklad matklad Oct 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be against WORM terminology, as it seems rather obscure: I don't think I see it often enough to not consult wikipedia. A more appropriate term would probably be SingleAssignmentCell, but that's a mouthful.

Ultimately, I doubt that there's a perfect solution for cell/sync naming, so the libs team would have to just decide on some particular naming scheme. I am personally ok with Sync prefix. The disadvantage of SyncX scheme is that it makes more common case longer to spell, and is a tautology (sync::SyncOnceCell).

I personally like the proposed cell::OnceCell / sync::OnceCell variant the most:

  • if you want extra clarity, you can qualify the type with cell:: or sync:: on the call site, instead of importing it
  • due to how awesome Rust is, it's impossible to use cell::OnceCell instead of sync::OnceCell, and the mistake in other direction is not really a mistake: just a very slight perf loss
  • in practice, I think it would be rare for a module to use cell and sync versions simultaneously.

Copy link

@nixpulvis nixpulvis Oct 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I like the way it's organized in the once_cell crate with explicit sync::* and unsync::* modules.

The part that feels the most strange to me about this currently proposal is that a OnceCell lives in both cell (makes sense, since it's a type of cell), and sync (makes sense, given that Once is also defined here). Something's conflated here, though this could well be outside the scope of this RFC. Either way, this is the first "cell" added to the sync module.

For another point of reference, we currently have Rc defined in std::rc and a similar Arc defined in std::sync. I might argue this would make more sense as std::rc::{Rc, sync::Arc}, which would solve my issue with OnceCell as well, though I don't expect this to be possible with backwards compatibility requirements.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, another possibility is to introduce something like lazy and lazy::sync modules. As it stands, std::sync today feels a bit like, well, kitchen sync, it has a bit of everything.

I can imagine an alternative world, where std::sync is only for synchronization primitives, atomic and mpsc are top-level modules and Arc lives in rc. In this world, having an std::lazy module would definitely be better.

With current stdlib structure, std::sync::OnceCell blends better with the background.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, everything in std::cell is !Sync so it sort of maps to crates.io’s once_cell::unsync.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The libs team has discussed moving stuff out of std::sync into more appropriate modules (e.g. std::mutex::Mutex). I think ideally we wouldn't throw more stuff in there.

@wolfiestyle
Copy link

I was expecting this to implement haskell-style lazy values. Can I use this outside global scope? Maybe use another more descriptive name? like LazyStatic

Copy link
Contributor

@tgross35 tgross35 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some changes from Once/Lazy to LazyCell/OnceCell/LazyLock/OnceLock, updated atomic ordering conclusion section

text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
text/0000-standard-lazy-types.md Outdated Show resolved Hide resolved
- Update LazyCell/LazyLock/OnceCell/OnceLock usage
- Clarify stance on atomics
- Some minor flow updates

Co-authored-by: bl-ue <54780737+bl-ue@users.noreply.github.com>
KnucklesAni added a commit to yace-project/yace that referenced this pull request Dec 25, 2022
Once_cell seems to be preferable method with path toward stabilization
via rust-lang/rfcs#2788
@tgross35
Copy link
Contributor

Now that rust-lang/rust#105587 is nearing the end of this FCP, could this be either merged or closed?

I would vouch for merging even if it isn't needed for any direct action. I see RFCs as a good way to document the intended use case - kind of nice to be able to look back at to see "what was the goal with this addition". But I also have no insight or sway into the RFC process so just do as is needed 🙂

@matklad
Copy link
Member Author

matklad commented Mar 30, 2023

As this is already stable on master (rust-lang/rust#105587), I've updated links and will merge the RFC, it seems to be sufficiently thoroughly accepted and does not merit an extra FCP and proccess.

Special thanks to @tgross35 for pushing this over the finish line, Rust needs more heroes like you!

@matklad matklad merged commit 2f1903c into rust-lang:master Mar 30, 2023
@matklad matklad deleted the std-lazy branch March 30, 2023 15:24
@tgross35
Copy link
Contributor

You did all the hard work @matklad, I just did the easy part :)

@dbsxdbsx
Copy link

dbsxdbsx commented Mar 31, 2023

As this is already stable on master (rust-lang/rust#105587), I've updated links and will merge the RFC, it seems to be sufficiently thoroughly accepted and does not merit an extra FCP and proccess.

Special thanks to @tgross35 for pushing this over the finish line, Rust needs more heroes like you!

Nice to hear this. Does that mean users could take the new official version to replace lazy_static and once_cell in the next version(1.69)?

@tgross35
Copy link
Contributor

tgross35 commented Mar 31, 2023

Nice to hear this. Does that mean users could take the new official version to replace lazy_static and once_cell in the next version(1.69)?

OnceCell and OnceLock will be stable in 1.70, and does replace once_cell::{sync, unsync}::OnceCell (exception of the try functions, see #109737). LazyCell and LazyLock, which are the direct replacements for lazy_static, are not yet going stable: see rust-lang/rust#109736

edit: LazyCell and LazyLock will be stable in 1.80 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-cell Proposals relating to interior mutability. A-sync Synchronization related proposals & ideas A-types-libstd Proposals & ideas introducing new types to the standard library. Libs-Tracked Libs issues that are tracked on the team's project board. T-libs-api Relevant to the library API team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.