2

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?

BitTickler
  • 10,905
  • 5
  • 32
  • 53

1 Answers1

5

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.

  • I think too much is explicit in Rust which only burdens users, while it could actually happen "behind the scenes". The concept of a pointer is probably the working theory for anyone starting to use Rust if it comes to passing by reference. As such, the closures captures in the code would be: a Reference to an u32. It is not easy to see why that should be a problem. And what most would expect would be that the compiler checks the code written, not a space of possible applications of ``make_counter()``. Only if someone violates the lifetime, the compiler should complain, imho. – BitTickler Sep 12 '15 at 09:13
  • 4
    @BitTickler This is a huge debate, but here's a few points: (1) Once you know your way around it, 99% of the time appeasing the borrow checker has very little cognitive overhead. (2) Declaring the lifetime relationships up front in the function signature is valuable documentation (if you know how to read it, of course) and (3) Not thinking about it and letting it be inferred will result in even more confusing error messages when caller and function disagree about a lifetime. It will also allow people to write "wrong" functions and not get an error until they call them in tricky circumstances. –  Sep 12 '15 at 09:59
  • Clearly, there is no right and wrong in this debate. But is it not amusing, that type inference and omission of giving types is quite popular, right now, yet "lifetime inference" vs "explicit lifetime specification" is still an open question? – BitTickler Sep 12 '15 at 10:29
  • 5
    @BitTickler Type inference is generally good, but the consensus of communities experienced with whole-program type inference (Haskell, ML variants) is that it's better to type annotate top-level definitions by default. The situation is similar in Rust, within each function lifetimes are mostly inferred, but there is no cross-function inference. There is lifetime *elision*, which serves the same purpose to a degree but is simpler, easily predictable, and local (only depends on the rest of the signature, not on other functions). –  Sep 12 '15 at 10:54
  • @BitTickler some of that is touched upon in [this question](http://stackoverflow.com/questions/31609137/why-are-explicit-lifetimes-needed-in-rust/31612025#31612025) – Shepmaster Sep 12 '15 at 13:52