3

I'm trying to implement lazy "thunks" in Rust and I just can't figure out how to get my code to pass the borrow checker. The basic idea is that a Thunk<T> can only be in one of two ThunkStates:

  1. Forced which carries its value of type T;
  2. Unforced, which carries a boxed closure that returns a T.

My naïve code goes like this:

pub struct Thunk<T>(ThunkState<T>);

enum ThunkState<T> {
    Forced(T),
    Unforced(Box<Fn() -> T>),
}

impl<T> Thunk<T> {
    pub fn new<F>(f: F) -> Thunk<T>
    where
        F: Fn() -> T + 'static,
    {
        Thunk(ThunkState::Unforced(Box::new(f)))
    }

    pub fn get(&mut self) -> &T {
        match self.0 {
            ThunkState::Forced(ref t) => &t,
            ThunkState::Unforced(ref f) => {
                // TROUBLE HERE
                self.0 = ThunkState::Forced(f());
                self.get()
            }
        }
    }
}

I get the following two compilation errors:

error[E0506]: cannot assign to `self.0` because it is borrowed
  --> src/main.rs:21:17
   |
19 |             ThunkState::Unforced(ref f) => {
   |                                  ----- borrow of `self.0` occurs here
20 |                 // TROUBLE HERE
21 |                 self.0 = ThunkState::Forced(f());
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ assignment to borrowed `self.0` occurs here

error[E0502]: cannot borrow `*self` as mutable because `self.0.0` is also borrowed as immutable
  --> src/main.rs:22:17
   |
19 |             ThunkState::Unforced(ref f) => {
   |                                  ----- immutable borrow occurs here
...
22 |                 self.get()
   |                 ^^^^ mutable borrow occurs here
23 |             }
24 |         }
   |         - immutable borrow ends here

I've gone through various iterations of trying out stuff (e.g., match *self.0, using &mut in the ThunkState patterns, and a few variations), but try as I may, I can't figure out how to fix it.

  1. Am I attempting to do something that doesn't make sense?
  2. If not, what makes this example so tricky, and how do I get it right?

Staring at it a bit more, I've formulated the following hypothesis: the assignment to self.0 would invalidate the f reference in that match branch. Is this right? And if so, then how do I achieve what I'm trying to do—discard the closure after I use it?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Luis Casillas
  • 29,802
  • 7
  • 49
  • 102
  • 2
    Drive-by comment: The implementations of this that I've seen use a tristate, Forced(T), InProgress, Closure(F) – bluss Sep 06 '16 at 20:55
  • @bluss I had a look at somebody's code, and [it looks like `InProgress` may be meant a mechanism for detecting cyclical dependencies between thunks](https://github.com/reem/rust-lazy/blob/master/src/single.rs#L48). If you know of other examples I'd love to see them as well. – Luis Casillas Sep 06 '16 at 22:30
  • 1
    I don't agree. I think they use it so that they have something to use in the `replace` call, where they replace it with EvaluationInProgress. – bluss Sep 07 '16 at 07:43

4 Answers4

3

Your original code works as-is with non-lexical lifetimes enabled (available in the 2018 edition):

pub struct Thunk<T>(ThunkState<T>);

enum ThunkState<T> {
    Forced(T),
    Unforced(Box<Fn() -> T>),
}

impl<T> Thunk<T> {
    pub fn new<F>(f: F) -> Thunk<T>
    where
        F: Fn() -> T + 'static,
    {
        Thunk(ThunkState::Unforced(Box::new(f)))
    }

    pub fn get(&mut self) -> &T {
        match self.0 {
            ThunkState::Forced(ref t) => t,
            ThunkState::Unforced(ref f) => {
                self.0 = ThunkState::Forced(f());
                self.get()
            }
        }
    }
}

This is now supported because the tracking of what is borrowed in which match arm is now more precise.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Awesome, this issue is one of my biggest pain points with the language, having to fight against the borrow checker with blocks like this which are clearly safe. For others interested, progress of the NLL feature is being tracked at https://github.com/rust-lang/rust/issues/43234 – ayoon Mar 10 '19 at 02:36
  • 1
    @ayoon note that this code works in stable Rust 2018. There are some corners of NLL that aren't fully implemented, but most of the big points are. – Shepmaster Mar 10 '19 at 15:35
2

This is indeed a tricky problem, but it is possible. For stuff like this, it's often a good idea to search for helpful functions in the mem module.

I've come up with a solution, but I think that there is still a lot of room for improvement.

pub fn get(&mut self) -> &T {
    let mut f = None;

    if let ThunkState::Unforced(ref mut f_ref) = self.0 {
        f = Some(std::mem::replace(f_ref, unsafe {
            std::mem::uninitialized()
        }));
    }

    if let Some(f) = f {
        self.0 = ThunkState::Forced(f());
    }

    match self.0 {
        ThunkState::Forced(ref t) => &t,
        _ => unreachable!(),
    }
}

This compiles fine, at least. The trick is to use mem::replace to get the important value out of self first. Additionally, you can avoid the unsafe by creating some kind of dummy value (like Box::new(|| panic!())).

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
0

I found something that semi-works:

pub struct Thunk<T>(ThunkState<T>);

enum ThunkState<T> {
    Forced(T),
    Unforced(Box<Fn() -> T>),
}

impl<T> Thunk<T> {
    pub fn new<F>(f: F) -> Thunk<T>
    where
        F: Fn() -> T + 'static,
    {
        Thunk(ThunkState::Unforced(Box::new(f)))
    }

    pub fn get(&mut self) -> &T {
        match self.0 {
            ThunkState::Forced(ref t) => &t,

            // Don't actually bind a variable to the boxed closure here.
            ThunkState::Unforced(_) => {
                self.0 = ThunkState::Forced(self.0.compute());
                self.get()
            }
        }
    }
}

impl<T> ThunkState<T> {
    fn compute(&self) -> T {
        match self {
            &ThunkState::Unforced(ref f) => f(),
            // FIXME: get rid of this panic?
            &ThunkState::Forced(_) => panic!("case never used"),
        }
    }
}

It compiles, but after trying to use this type what I've learned is that I probably need interior mutability.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Luis Casillas
  • 29,802
  • 7
  • 49
  • 102
  • If you are sure the `Forced` case can't occur, you can change the `panic!()` to `unreachable!()`. – ljedrz Sep 07 '16 at 05:16
  • This works fine since the thunk is using an `Fn()` (call-many through shared reference), like in the question. – bluss Sep 07 '16 at 08:52
0

The problem is that you're trying to do too much (for the sake of avoiding return) within the lexical borrowing context of match self.0 { ... }.

What you can do is:

  1. Move results of calculations performed on values you need to borrow from self.0 into variables defined in an outer scope.
  2. Exit early on paths where those values aren't needed
  3. Consume the values after the match statement

Applied to your example, a solution could be:

pub struct Thunk<T>(ThunkState<T>);

enum ThunkState<T> {
    Forced(T),
    Unforced(Box<Fn() -> T>),
}

impl<T> Thunk<T> {
    pub fn new<F>(f: F) -> Thunk<T>
    where
        F: Fn() -> T + 'static,
    {
        Thunk(ThunkState::Unforced(Box::new(f)))
    }

    pub fn get(&mut self) -> &T {
        let func_result: T;
        match self.0 {
            ThunkState::Forced(ref t) => {
                return &t;
            }
            ThunkState::Unforced(ref f) => {
                func_result = f();
            }
        }
        self.0 = ThunkState::Forced(func_result);
        self.get()
    }
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Sufian
  • 8,627
  • 4
  • 22
  • 24