Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to declare a closure that lives longer than its enclosing block

I suppose this question is about lifetimes in general, but I'm having difficulty with closures specifically because you can't write out their type.

This example is a bit contrived - I'm just starting to learn Rust, and this is something I've been hung up on.

This program won't compile:

fn main () {
    let mut list: Vec<&Fn() -> i32> = Vec::new();

    {
        list.push(&|| 1);
    }
}

Because:

src/main.rs:5:25: 5:24 error: borrowed value does not live long enough
src/main.rs:5         list.push(&|| 1);
                                 ^~~~
src/main.rs:2:50: 7:2 note: reference must be valid for the block suffix following statement 0 at 2:49...
src/main.rs:2     let mut list: Vec<&Fn() -> i32> = Vec::new();
src/main.rs:3
src/main.rs:4     {
src/main.rs:5         list.push(&move || 1);
src/main.rs:6     }
src/main.rs:7 }
src/main.rs:5:9: 5:26 note: ...but borrowed value is only valid for the statement at 5:8
src/main.rs:5         list.push(&|| 1);
                      ^~~~~~~~~~~~~~~~~
src/main.rs:5:9: 5:26 help: consider using a `let` binding to increase its lifetime
src/main.rs:5         list.push(&|| 1);
                      ^~~~~~~~~~~~~~~~~

What I gather from this error is that the closure's lifetime is limited to the statement inside the block, but it needs to live for the entire body of main.

I know (or, I think) that passing the closure to push as a reference means that push is only borrowing the closure, and that ownership will be returned to the block. This code would work if I could just give the closure to push (i.e. if push took ownership of the closure), but since a closure isn't sized, I must pass it as a reference.

Is that right? How can I make this code work?

like image 655
Carson Myers Avatar asked May 29 '15 04:05

Carson Myers


2 Answers

There are two things you are asking about:

  1. specifying a typename for something that has no specifyable typename
  2. letting a closure live longer than the block where it's defined.

The first issue is fixed by NOT specifying the typename, and letting rust's type inference do the work.

let mut list: Vec<_> = Vec::new();

The second issue is fixed by not trying to make the closure live longer, but by making it "by value" so you can move it. This enforces that your closure does not reference anything, but owns all the captured values.

for i in 0..10 {
    list.push(move || i);
}

Now this gives us a new problem. If we add a different closure to the Vec, the types won't match. Therefore to achieve that, we need to box the closures.

fn main () {
    let mut list: Vec<Box<Fn() -> i32>> = Vec::new();

    for i in 0..10 {
        list.push(Box::new(move|| i));
    }

    {
        list.push(Box::new(move|| 42));
    }
}
like image 193
oli_obk Avatar answered Sep 20 '22 02:09

oli_obk


Borrows do not own the thing they point to. Your problem is that you're borrowing a temporary which is going to cease to exist right after it's borrowed because you haven't stored it anywhere. If it helps, consider that borrows don't borrow values, they borrow storage, and a temporary has only transient storage.

If you want a borrow to something to last for any given period, you must borrow from storage that will last at least that long. In this case, because you want to store the borrow in a Vec, this means that whatever storage you borrow from must outlive the Vec as well. Thus:

fn main () {
    let closure;
    let mut list: Vec<&Fn() -> i32> = Vec::new();

    {
        closure = || 1;
        list.push(&closure);
    }
}

Note that closure is defined before list is. In Rust, values are dropped in reverse lexical order at the end of their scope, so any variable defined after list will necessarily be dropped before it, thus leading to list containing invalid pointers.

If you want to push multiple closures, you will need a separate variable for each one.

To forestall a possible "my actual problem isn't this simple" addendum (:P): f you need to return list or in some way persist it beyond a single function call, note that there is no way to extend a borrow. In that case, what you need to do is change list to a vector of owned, boxed closures (i.e. Vec<Box<Fn() -> i32>>).

like image 24
DK. Avatar answered Sep 22 '22 02:09

DK.