matklad / once_cell

Rust library for single assignment cells and lazy statics without macros

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Generalising Lazy::force to non-functional types

01mf02 opened this issue · comments

I need to define a function that constructs a lazy value based on some input, such as follows:

fn upper_lazy1(s: String) -> Lazy<String> {
    Lazy::new(|| s.to_uppercase())
}

However, the compiler complains that it "expected fn pointer, found closure".
Good. So I could return Lazy<String, impl FnOnce() -> String> instead, but this makes it difficult to store this lazy value in a struct (due to the impl). Or I could box the closure, but then I get unwanted overhead, and I have to wrestle with lifetimes.

It would be nice if I could write something like:

struct Uppercase(String);

impl Into<String> for Uppercase {
    fn into(self) -> String {
        self.0.to_uppercase()
    }
}

fn upper_lazy(s: String) -> Lazy<String, Uppercase> {
    Lazy::new(Uppercase(s))
}

fn main() {
    let lazy = upper_lazy("Hello, world!".to_string());
    // the next line does not work
    assert_eq!(Lazy::force(&lazy), "HELLO, WORLD!");
}

However, the current definition of the Lazy::force function does not permit this, because it demands that
in Lazy<T, F>, F implements FnOnce() -> T.

So I see a few possible alternative bounds for F in Lazy::force to support my use case:

  • FnOnce() -> T:
    This is the current solution chosen by Lazy. This works only for initialising lazy values with functions/closures AFAIK.
    It might support my use case eventually if I could implement FnOnce for custom types such as Uppercase, but
    this is currently unstable and has been discussed since more than five years ...
    rust-lang/rust#29625
  • Into<T>:
    This supports my use case perfectly.
    However, it excludes FnOnce, because Into<T> is not implemented for FnOnce() -> T.
    (Is there any chance of Into<T> getting implemented for FnOnce() -> T one day?)
  • A new trait, called e.g. Evaluate<T>:
    This trait has the same signature as Into<T>, but because it is a new trait,
    it can be implemented for FnOnce() -> T as well as for custom types such as Uppercase.
    That's what I am doing right now, but this might be ugly in case that one of the two previous options becomes viable eventually.

The Evaluate<T> approach can be implemented as follows:

    pub trait Evaluate<T> {
        fn evaluate(self) -> T;
    }
    
    impl<T, F: FnOnce() -> T> Evaluate<T> for F {
        fn evaluate(self) -> T {
            self()
        }
    }

    // adapted from current code in `once_cell`
    impl<T, F: Evaluate<T>> Lazy<T, F> {
        pub fn force(this: &Lazy<T, F>) -> &T {
            this.cell.get_or_init(|| match this.init.take() {
                Some(f) => f.evaluate(),
                None => panic!("Lazy instance has previously been poisoned"),
            })
        }
    }

What do you think about generalising Lazy to support use cases like mine?

Lazy is just a thin convenience layer over OnceCell. If lazy doesn’t work for your particular use case, you can build arbitrary abstractions on top of OnceCell.

I know. However, it would be a pity to restrict Lazy to types implementing FnOnce if there was a more general way ...

I thought of another way than modifying the trait bounds of force: Introduce a new function, e.g. force_with, which takes a function that evaluates the initial value given to Lazy::new.

    impl<T, F> Lazy<T, F> {
        pub fn force_with<G: FnOnce(F) -> T>(&self, g: G) -> &T {
            self.cell.get_or_init(|| match self.init.take() {
                Some(f) => g(f),
                None => panic!("Lazy instance has previously been poisoned"),
            })
        }
    }

    impl<T, F: FnOnce() -> T> Lazy<T, F> {
        pub fn force(&self) -> &T {
            self.force_with(|f| f())
        }
    }

Example usage:

fn main() {
    let lazy = Lazy::new("Hello, world!".to_string());

    assert_eq!(
        Lazy::force_with(&lazy, |s| s.to_uppercase()),
        "HELLO, WORLD!"
    );
}

This approach is strictly more permissive than the current one in Lazy and is backwards-compatible.
Furthermore, this allows the lazy value to be evaluated with different functions, depending on where it is evaluated.

However, it would be a pity to restrict Lazy to types implementing FnOnce if there was a more general way ...

Maximal generality is a non-goal for once_cell. Rather, we strive for the simplest API which can serve as building block for most of use-cases.

What you are describing is I think a LazyTransform. It is indeed somewhat more general that OnceCell (not strictly more general, as it requires poisoning), but it is a rather niche thing, and can be implemented on top of once cell:

    pub struct OnceTransform<U, T> {
        u: Cell<Option<U>>,
        t: OnceCell<T>,
    }

    impl<U, T> OnceTransform<U, T> {
        pub fn get_or_init(&self, f: impl FnOnce(U) -> T) -> &T {
            self.t.get_or_init(|| {
                let u = self.u.take().expect("poisoned");
                f(u)
            })
        }
    }

So, provide it here doesn't fit the design goals of once_cell


That being said, the API of OnceCell has been ironed out quite a bit, and is on path to inclusion in std, so I think we can tolerate a bit of a scope creep now. OnceCell-based impl of OnceTransform would suffer from two drawbacks:

  • a smudge of (tricky) unsafe is needed to make it Sync
  • The repr would store an (Option<T>, Option<U>) rather than an Option<Either<T, U>>.

Adding a fresh type for that under an unstable flag would be OK.

It seems that this issue will be resolved when the either fn_traits or type_alias_impl_trait feature is stable.

Hm, I see. Well, then I think that my proposition does not make so much sense.
Thanks for taking your time to discuss this with me! :)