Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do I initialize a Lazy with a closure that captures a variable? (With fun recursion question) #156

Closed
chriscoomber opened this issue Jul 30, 2021 · 6 comments

Comments

@chriscoomber
Copy link

chriscoomber commented Jul 30, 2021

1a) So I understand that I can do:

let lazy: Lazy<String> = Lazy::new(|| String::from("hello"));

Because:

  • When you don't specify the 2nd generic it assumes fn() -> String
  • This closure coerces to fn() -> String

1b) I also understand that I can do:

let data = String::from("hello");
let lazy: Lazy<String, _> = Lazy::new(move || data);

Because:

  • This closure doesn't coerce to fn() -> String, since it captures something
  • However, if I use _ I can let the type system infer the type of the closure itself (closures are a Voldemort type, one cannot name their actual type in code so I couldn't replace _ with the true type even if I tried).

2a) Now what if I wanted to put the Lazy into a struct?

struct Foo(Lazy<String>);
let foo = Foo(Lazy::new(|| String::from("hello")));

Works fine

2b) But how would I do this in the case where my closure captures something?

struct Foo(Lazy<String>);
let data = String::from("hello");
let foo = Foo(Lazy::new(move || data));  // Compile error: closure doesn't coerce to `fn() -> String`

_ doesn't work. Am I forced to do something like this?

struct Foo<F: FnOnce() -> String>(Lazy<String, F>);
let data = String::from("hello");
let foo = Foo(Lazy::new(move || data));

This isn't always convenient.
Start Edit A much simpler example than what I write below is something like

struct Foo<F: FnOnce() -> String>(Lazy<String, F>);
let data = String::from("hello");
let foos = vec![
    Foo(Lazy::new({ let data_clone = data.clone(); move || data_clone + "1" })),
    Foo(Lazy::new({ let data_clone = data.clone(); move || data_clone + "2" })),
];

This doesn't compile because I want the two Foo<F>s to be the same type in order to put them in the vec, but they use different closures so they are not the same type.

End Edit

For example, if Foo is recursive in any way (e.g. a lazy-tree, which is how I came across this):

struct Foo {
    value: i32,
    left: Option<Box<Lazy<Foo>>>,
    right: Option<Box<Lazy<Foo>>>,
}

let data = 17;
let foo = Foo {
    value: data,
    left: Some(Box::new(Lazy::new(move || Foo { value: data * 2, left: None, right: None }))),
    right: Some(Box::new(Lazy::new(move || Foo { value: data *2 + 1, left: None, right: None }))),
};

There's no way I can add the function generic to Foo any more: struct Foo<F: FnOnce() -> Foo<F>>. This is recursive, which is apparently fine on its own but it causes problems because once F is resolved it can only match a single closure. This means that all of the Lazys all the way down need to use the same closure - which I obviously don't want.

So, am I forced to use Lazy<Foo, Box<dyn FnOnce() -> Foo>> and box up all my closures? This is what I've got working but is there no nicer way?

struct Foo {
    value: i32,
    left: Option<Box<Lazy<Foo, Box<dyn FnOnce() -> Foo>>>>,
    right: Option<Box<Lazy<Foo, Box<dyn FnOnce() -> Foo>>>>,
}

let data = 17;
let foo = Foo {
    value: data,
    left: Some(Box::new(Lazy::new(
        Box::new(move || Foo { value: data * 2, left: None, right: None }) as Box<dyn FnOnce() -> Foo>
    ))),
    right: Some(Box::new(Lazy::new(
        Box::new(move || Foo { value: data * 2 + 1, left: None, right: None }) as Box<dyn FnOnce() -> Foo>
    ))),
};
@matklad
Copy link
Owner

matklad commented Jul 30, 2021

Most of the time, if you want to store lazy in a field, it’s better to use once cell directly. See “config” example in the crate docs.

Seems like we need to document on Lazy that sometimes you don’t need it….

@chriscoomber
Copy link
Author

chriscoomber commented Jul 30, 2021

I quite like the interface of Lazy though - I want to provide the initializer when I first use it only, not every time I use it. Perhaps that means we ultimately need the Box<dyn ....

I found this quite useful for reducing the amount I had to write. I don't know whether it makes sense to add something like this...

pub type LazyBoxedInit<T> = Lazy<T, Box<dyn 'static + FnOnce() -> T>>;

impl<T> LazyBoxedInit<T> {
    pub fn new_boxed_init<G: 'static + FnOnce() -> T>(init: G) -> Self {
        Lazy::new(Box::new(init))
    }
}

@BlackGlory
Copy link

This issue seems to be related to #90.

I tried to implement Lazy myself, but didn't solve the problem, because I ended up with an implementation very similar to once_cell's Lazy. In my opinion, this is a flaw in the Rust type system.

@mahor1221
Copy link

mahor1221 commented Sep 7, 2022

edit: I just realized this usage of Deref is an anti-pattern:
https://rust-unofficial.github.io/patterns/anti_patterns/deref.html

Chriscoomber's answer was quite useful for me. I've created a simple wrapper around Lazy to use LazyBoxedInit

use std::fmt;
use std::ops::{Deref, DerefMut};

type Lazy<T> = once_cell::unsync::Lazy<T, Box<dyn FnOnce() -> T + 'static>>;
pub struct LazyBoxedInit<T>(Lazy<T>);

impl<T> LazyBoxedInit<T> {
   pub fn new<F: FnOnce() -> T + 'static>(init: F) -> Self {
       Self(Lazy::new(Box::new(init)))
   }
}

impl<T> Deref for LazyBoxedInit<T> {
   type Target = Lazy<T>;
   fn deref(&self) -> &Self::Target {
       &self.0
   }
}

impl<T> DerefMut for LazyBoxedInit<T> {
   fn deref_mut(&mut self) -> &mut Lazy<T> {
       &mut self.0
   }
}

impl<T: fmt::Debug> fmt::Debug for LazyBoxedInit<T> {
   fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
       self.0.fmt(f)
   }
}

@matklad
Copy link
Owner

matklad commented Oct 22, 2022

I think there isn't much we can do here, so closing. That being said, it seems that this is a common an unfortunate footgun, so a PR to docs would be appreciated

@matklad matklad closed this as completed Oct 22, 2022
@NobodyXu
Copy link

NobodyXu commented Oct 23, 2022

I think there isn't much we can do here, so closing. That being said, it seems that this is a common an unfortunate footgun, so a PR to docs would be appreciated

I think the support of "impl Trait" in variable in the language will solve this.
RFC
rust-lang/rust#63065

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants