2
votes

Gave VisualRust another try and to see how far they got, I wrote a few lines of code. And as usual, the code causes me to write a question on stackoverflow...

See first, read my question later:

fn make_counter( state : &mut u32  ) -> Box<Fn()->u32> 
{
    Box::new(move || {let ret = *state; *state = *state + 1; ret })
}

fn test_make_counter() {
    let mut cnt : u32 = 0;
    {
        let counter = make_counter( & mut cnt );
        let x1 = counter();
        let x2 = counter();
        println!("x1 = {}  x2 = {}",x1,x2);
    }
}

fn alt_make_counter ( init : u32 ) -> Box<Fn()->u32> {
    let mut state = init;
    Box::new(move || {let ret = state; state = state + 1; ret })
}   


fn test_alt_make_counter() {
    let counter = alt_make_counter( 0u32 );
    let x1 = counter();
    let x2 = counter();
    println!("x1 = {}  x2 = {}",x1,x2);
}

fn main() {
    test_make_counter();
    test_alt_make_counter();
}

The difference between make_counter() and alt_make_counter() is, that in one case, the state is a pointer to a mutable u32 passed to the function and in the other case, it is a mutable u32 defined inside the function. As the test_make_counter() function shows clearly, there is no way, that the closure lives longer than the variable cnt. Even if I removed the block inside test_make_counter() they would still have the identical lifetime. With the block, the counter will die before cnt. And yet, Rust complains:

src\main.rs(4,2): error : captured variable state does not outlive the enclosing closure src\main.rs(3,1): warning : note: captured variable is valid for the anonymous lifetime #1 defined on the block at 3:0

If you look at the alt_make_counter() function now, the lifetime of state should basically cause the same error message, right? If the code captures the state for the closure, it should not matter if the pointer is passed in or if the variable is bound inside the function, right? But obviously, those 2 cases are magically different.

Who can explain, why they are different (bug, feature, deep insight, ...?) and if there is a simple rule one can adopt which prevents wasting time over such issues now and then?

1

1 Answers

5
votes

The difference is not in using a local variable vs. using a parameter. Parameters are perfectly ordinary locals. In fact, this version of alt_make_counter works1:

fn alt_make_counter (mut state: u32) -> Box<FnMut() -> u32> {
    Box::new(move || {let ret = state; state = state + 1; ret })
}

The problem is that the closure in make_counter closes over a &mut u32 instead of u32. It doesn't have its own state, it uses an integer somewhere else as its scratch space. And thus it needs to worry about the lifetime of that location. The function signature needs to communicate that the closure can only work while it can still use the reference that was passed in. This can be expressed with a lifetime parameter:

fn make_counter<'a>(state: &'a mut u32) -> Box<FnMut() -> u32 + 'a> {
    Box::new(move || {let ret = *state; *state = *state + 1; ret })
}

Note that 'a is also attached to the FnMut() -> u32 (though with a different syntax because it's a trait).

The simplest rule to avoid such trouble is to not use references when they cause problems. There is no good reason for this closure to borrow its state, so don't do it. I don't know whether you fall under this, but I've seen a bunch of people that were under the impression that &mut is the primary or only way to mutate something. That is wrong. You can just store it by value and then just mutate that directly by storing it, or the larger structure in which it is contained, in a local variable that is tagged as mut. A mutable reference is only useful if the results of the mutation needs to be shared with some other code and you can't just pass the new value to that code.

Of course, sometimes juggling references in complicated ways is necessary. Unfortunately there doesn't seem to be a quick and easy way to learn to deal with those confidently. It's a big pedagogic challenge, but so far it appears everyone just struggled for a while and then progressively had fewer problems as they get more experienced. No, there is no single simple rule that solves all lifetime woes.

1 The return type has to be FnMut in all cases. You just didn't get an error about that yet because your current error happens at an earlier stage in the compilation.