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!
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 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?