Capturing a copy of a local variable for a lambda

At work someone ran into an issue that surprised them with Python. This prints "message 2" three times:

funs_to_execute = []
for i in range(3):
    funs_to_execute.append(lambda: print(f"message {i}"))

for fun in funs_to_execute:
    fun()

I've written very little rust but I have been interested to start learning it. I decided to see what this code might look like in rust:

fn main() {
    let mut vec = Vec::new();
    for i in 0..3 {
        vec.push(|| println!("message {}", i));
    }
    for fun in vec.iter() {
        fun();
    }
}

Which of course fails to compile because it can identify that the i in the capture "does not live long enough".

In Python the fix is typically something like this:

funs_to_execute = []
for i in range(3):
    funs_to_execute.append(lambda i=i: print(f"message {i}"))

for fun in funs_to_execute:
    fun()

So we essentially save off the value of i for each iteration by having it be set to the default value of an argument of the lambda.

My question is: How would you fix this Rust code to print "message 0", "message 1", "message 2"? I tried a few things and I wasn't able to figure it out.

1 Like

For example, I tried creating a local variable in the capture, which presumably would have the correct lifetime. But I can't copy the current value of i into that variable. This doesn't fix anything:

fn main() {
    let mut vec = Vec::new();
    for i in 0..3 {
        vec.push(|| {
            let x = i;
            println!("message {}", x);
        });
    }
    for fun in vec.iter() {
        fun();
    }
}

I also tried creating the loop variable outside of the for loop, but that still doesn't work:

fn main() {
    let mut vec = Vec::new();
    let mut i = 0;
    while i < 3 {
        vec.push(|| {
            let x = i;
            println!("message {}", x);
        });
        i += 1;
    }
    for fun in vec.iter() {
        fun();
    }
}

For some reason this gives the error: "assignment to borrowed i occurs here" ... But i shouldn't be borrowed here. Unless just copying the value of i into x borrows it?

I guess that's the case. I have no idea how to make this code work in Rust :frowning:

You can use move closures to prevent closures borrow its captures.

vec.push(move || println!("message {i}"));

https://doc.rust-lang.org/stable/book/ch13-01-closures.html#capturing-references-or-moving-ownership

9 Likes

I think it's worth noting that the fact that this program fails to compile whereas the analogous Python runs but gives the wrong answer is exactly what Rust's ownership and borrowing system is about. Prior languages gave you either the ability to accidentally use invalid pointers (C, C++, etc.) or implicitly shared pointers (Python, Java, etc.). Rust, by sticking to only exclusive mutability (except when explicitly opted out of), prevents this mistake (using i in the closure after it has been mutated elsewhere) by default.

(The particular error happened to be about & reference lifetimes, not borrow conflicts, but Rust wouldn't have & that works the way it does if it weren't for the ownership system.)

Of course, there are also other ways to solve this particular problem. Python's for could have had semantics which declare a new i variable (which each closure would capture separately) instead of the loop iterations assigning to a single existing variable.

But i shouldn't be borrowed here. Unless just copying the value of i into x borrows it?

Non-move closures always take the least powerful kind of capture (& borrow < &mut borrow < move) they can. Copying a value only requires a & borrow, so that's what it picks. If i was of a type that does not implement Copy (say, a String), then it would have been moved instead.

As already mentioned, using a move closure is the solution. Beyond that, sometimes one finds the need to write explicit variable declarations to control exactly what a closure captures, somewhat like you tried — but the key is that to be effective, those must be written outside the closure, not inside, and the closure must (for most of the cases where this arises) be a move closure. That would look like:

        vec.push({
            let x = i.clone();
            move || println!("message {}", x)
        });

In this case, there's no reason to do that, since i is Copy and the move closure suffices to handle that — but I thought I'd mention this common pattern that you almost stumbled on.

13 Likes

Oh, it was the default behavior of the closure! @kpreid very cool, yeah I was playing around with i.clone() and trying to get that to work too but I wasn't able to get it. Ty @Hyeonu too!

That's because once again, Clone only needs to borrow (the signature of Clone::clone() is: fn clone(&self) -> Self), therefore a closure only needs to borrow something that you clone inside of it.

Then I wonder why does this fail to compile:

fn drop_i32(_: i32) {}

fn main() {
    let mut vec = Vec::new();
    for i in 0..3 {
        vec.push(|| drop_i32(i));
    }
    for fun in vec.iter() {
        fun();
    }
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0597]: `i` does not live long enough
 --> src/main.rs:6:30
  |
6 |         vec.push(|| drop_i32(i));
  |         ---------------------^--
  |         |        |           |
  |         |        |           borrowed value does not live long enough
  |         |        value captured here
  |         borrow later used here
7 |     }
  |     - `i` dropped here while still borrowed

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` due to previous error

It should be clear to the compiler that i cannot be borrowed here because I expect an i32 and not an &i32 here. So the "least powerful kind of capture" would be moving. What do I misunderstand here?

i32 is Copy, so you can copy it into the drop_i32, even if you have only a shared reference. Therefore, shared borrow is enough.

4 Likes

@jbe incidentally, this is what @kpreid also wrote above:

2 Likes

Oh sorry, I didn't read/understand properly.

Copy is pretty confusing:

fn main() {
    let i = 5;
    drop(i); // useless!
    println!("{i}");
}

(Playground)

Output:

5

That behavior is the very definition of Copy.

3 Likes

In case you haven't come across std::mem::drop's docs before:

Disposes of a value.

This does so by calling the argument’s implementation of Drop.

This effectively does nothing for types which implement Copy, e.g. integers. Such values are copied and then moved into the function, so the value persists after this function call.

This function is not magic; it is literally defined as

pub fn drop<T>(_x: T) { }

Because _x is moved into the function, it is automatically dropped before the function returns.

Yes, I am aware that this makes sense in case of drop, but in context of a closure it's more irritating. I guess that's because here

i is of type i32, and not of type &i32. Yet the "capturing" mechanism captures only a reference to it. It seems all consistent to me now, but it's not easy to understand.

I guess many people write move in front of almost every closure.