matklad / once_cell

Rust library for single assignment cells and lazy statics without macros
Apache License 2.0
1.84k stars 110 forks source link

Typestate pattern #163

Closed imbolc closed 1 year ago

imbolc commented 2 years ago

Isn't it possible to get rid of Option in get() via typestate pattern (making the method available only for an initialized cell)?

matklad commented 2 years ago

You don't need typestate here -- if you statically know that &OnceCell<T> is initialized, you can use &T.

imbolc commented 2 years ago

By "statically know" you mean static N: Lazy<i32> = Lazy::new(|| 42);? If so it's almost all of my use cases require runtime initialization (like reading an parsing a file). Then I should match on .get()s option each time which is inconvenient, or use &T without compile time guaranties of initialization.

matklad commented 2 years ago

Could you give a small example of your use-case? Basically, what I think should work is to match on .get once, and then thread the resulting &T, but it's hard to know if that'll work without a specific example.

imbolc commented 2 years ago

The docs example will do: https://docs.rs/once_cell/1.8.0/once_cell/index.html#safe-initialization-of-global-data A call of Logger::global() before INSTANCE.set(..) will panic.

matklad commented 2 years ago

Yeah, if that's a concern, it is possible to remove global function at all, and pass &'static Logger to any code which needs a logger. &'static is Copy and doesn't have lifeitems, so that should be pretty ergonomic.

imbolc commented 2 years ago

Do you mean passing the reference as an argument? I thought the whole point of once_cell is having "globals". With passing through arguments why not just initialize the logger and use a usual reference?

matklad commented 2 years ago

Yeah, I mean passing as an argument. I think with any kind of typestate you'd have to pass an argument though?

imbolc commented 2 years ago

I thought of this, but I missed type changing in set() :) Maybe it can be done somehow, maybe by unsafe, because type essentially stay the same?

use std::marker::PhantomData;

struct Cell<T, S> {
    payload: Option<T>,
    state: PhantomData<S>,
}

struct Empty;
struct Full;

impl<T, S> Cell<T, S> {
    fn new() -> Cell<T, Empty> {
        Cell {
            payload: None,
            state: PhantomData,
        }
    }
}

impl<T> Cell<T, Empty> {
    fn set(&self, payload: T) -> Cell<T, Full> {
        // I thought I could somehow mess with self here instead of returning a new instance
        Cell {
            payload: Some(payload),
            state: PhantomData,
        }
    }
}

impl<T> Cell<T, Full> {
    fn get(&self) -> &T {
        self.payload.as_ref().unwrap()
    }
}

fn main() {
    let empty = Cell::<i32, Empty>::new();
    let full = empty.set(1);
    assert_eq!(full.get(), &1);
}
matklad commented 1 year ago

I think at this point its best to experiment with this kinds of API in a different crate!