[pre-RFC] lazy-static move to std

Thinking about it a bit more, I want to argue that:

  • std must provide solution for use-case covered by lazy_static
  • lazy_static per se is not the best solution to these use-cases
  • with the recent stabilization of calling const_fn and removal of &'static from std::sync::Once, std can provide the best solution.

The TL;DR conclusion is that we should stabilize lazy_cell and not lazy_static! :smiley:

Use Cases

Today lazy_static sovles two somewhat orthogonal use-cases:

  • lazy initialization
  • complex global data

Both of these use-case are pervasive and not domain specific (some language has build-in support for denoting laziness). Moreover, while they are easy to implement in other languages, in Rust they require unsafe code.

Note that const fn helps with the second use-case significantly, but still there are cases where you need runtime initialization (something like a global logger config, for example).

I feel pretty strongly that such generally useful unsafe code which expands the language vocabulary (as opposed to just making things faster) belongs to std.

Solution

Now, lazy_static! is not a really nice solution; suffices to point out that it is a macro. What if we could just have some sort of lazy value container, which could also be used as a static? I think we can have just that now (and such API was impossible before, due to &'static on Once and inability to call const fn).

Here’s my rough proposal, std::cell::OnceCell, a non-thread safe lazy value, and a std::sync::OnceCell thread-safe counterpart, both of which have const fn constructors. I don’t think that the following snippet is using unsafe correctly. I don’t even think it works, I’ve never tested it :slight_smile: However, I am 90% sure that we have all the necessary language machinery to fix the bugs. And these all are basically copy-paste from lazy_static and lazy_cell.

Playground (EDIT: now with Drop)

Using this API, the example from lazy_static readme could look like this:

use std::collections::HashMap;
use std::sync::OnceCell;

pub fn hashmap() -> &'static HashMap<u32, &'static str> {
    static HASHMAP: OnceCell<HashMap<u32, &'static str>> = OnceCell::new();

    HASHMAP.get_or_init(|| {
        let mut m = HashMap::new();
        m.insert(0, "foo");
        m.insert(1, "bar");
        m.insert(2, "baz");
        m
    })
}

Note that the primary difference with lazy_static API is that the init function is supplied at the access time, and not at the construction time. This is a feature, which makes API more flexible: you can now close over stack data to initialize a static lazy value. It should be possible to provide a more traditional API on top:

struct Lazy<T, F: Fn() -> T> {
    cell: sync::OnceCell<T>,
    init: F,
}
impl<T, F: Fn() -> T> Lazy<T, F> {
    const fn new(init: F) -> Lazy<T, F> {
        Lazy { cell: sync::OnceCell::new(), init }
    }
}
impl<T, F: Fn() -> T> ::std::ops::Deref for Lazy<T, F> {
    type Target = T;
    fn deref(&self) -> &T {
        self.cell.get_or_init(|| (self.init)())
    }
}

However, I specifically propose not to add such Lazy API to the std just yet: it won’t work out quite well because you’ll need to spell out that F type for global values, which is awkward.

So, how do folks feel about RFCing the addition of cell::OnceCell and sync::OnceCell to std? Or am I missing some gnarling safety/usability hole?

12 Likes