Trying to switch from OnceCell to Lazy

I made this minimal example:

use once_cell; // 1.17.0
use rand; // 0.8.5

use once_cell::sync::{OnceCell, Lazy};
use rand::rngs::StdRng;
use rand::{SeedableRng, Rng};

static A: OnceCell<i32> = OnceCell::new();
static B: OnceCell<i32> = OnceCell::new();

fn init() {
    let mut r = StdRng::seed_from_u64(12345);
    A.set(r.gen()).unwrap();
    B.set(r.gen()).unwrap();
}

fn main() {
    init();
    dbg!((A.get().unwrap(), B.get().unwrap()));
}

By trying to be more idiomatic (and remove a lot of .get().unwrap()) , I wanted to switch from OnceCell to Lazy since after their initialization neither A or B will change. The problem is that their initialization is order dependent, and main has to remain the same (so I cannot make a Lazy<(i32, i32)>) so the only solution I see would be to have something like static destructuring:

use once_cell; // 1.17.0
use rand; // 0.8.5

use once_cell::sync::{OnceCell, Lazy};
use rand::rngs::StdRng;
use rand::{SeedableRng, Rng};

static (A, B): Lazy<(i32, i32)> = Lazy::new(|| init());

fn init() {
    let mut r = StdRng::seed_from_u64(12345);
    A.set(r.gen()).unwrap();
    B.set(r.gen()).unwrap();
    (A, B)
}

fn main() {
    init();
    dbg!((A.get().unwrap(), B.get().unwrap()));
}

How would you achieve this ? Or maybe it's unnecessary and OnceCell are already good enough ?

What do you mean by this? If you mean that A and B must be initialized in that order, then that's exactly what you achieve by a Lazy<(i32, i32)>. Accordingly, this works predictably.

If you mean that for some weird reason, you need to syntactically preserve the variable names A and B as separate items, then just move the side-effect-less field getter logic into them, and let the initializer logic remain atomic.

2 Likes

If you still want to be able to access the values via A and B statics, you could also do something like this

Playground

use std::ops::Deref;

use once_cell::sync::{Lazy, OnceCell};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};

static BOTH: Lazy<(i32, i32)> = Lazy::new(|| {
    let mut r = StdRng::seed_from_u64(12345);
    (r.gen(), r.gen())
});

static A: LazyA = LazyA(&BOTH);
static B: LazyB = LazyB(&BOTH);

pub struct LazyA(&'static Lazy<(i32, i32)>);

impl Deref for LazyA {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &self.0 .0
    }
}

pub struct LazyB(&'static Lazy<(i32, i32)>);

impl Deref for LazyB {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &self.0 .1
    }
}

fn main() {
    dbg!((*A, *B));
}

Oh, I didn't know that you could initialize some Lazys with other Lazys. Thanks !

Why couldn't you? The point of a Lazy is that it allows you to execute arbitrary code upon first access. Surely that includes accessing other globals.

The definition of Lazy<T> says that the initializer must satisfy FnOnce() -> T, i.e., a function returning a T, and in particular the default initializer type is fn() -> T, i.e., a pointer to a function that returns a value of the given type. This doesn't forbid accessing other statics in the body of the initializer. It's just a regular, plain old function.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.