-
Notifications
You must be signed in to change notification settings - Fork 13.7k
Description
Unlike previously discussed inference of move
from lifetimes (which has been deemed unfeasible/undesirable), we can completely ignore lifetime bounds and NLL analysis.
Note: everything discussed here "trivially" doesn't pass lifetime inference/checks currently, so allowing such code to compile should be entirely backwards-compatible.
Instead, I think we should focus on expressions that had their types inferred to contain closure types, and compare their (lexical/"drop") scopes with the scopes the closure captures are declared in, e.g.:
let x = String::new();
let f = {
let y = vec![0];
Some(|| (x.clone(), y.clone()))
};
typeof f
is Option<closure>
, where the closure captures x
and y
, and the only way the closure could be capturing any of them by reference, is if their scopes contain the scope of f
.
This is true for x
(assuming f
is as far as the closure escapes), but not y
, and the closure should therefore capture x
by value, allowing that code to compile without explicit move
.
Another, perhaps more common example is .flat_map(|x| (0..n).map(|i| x + i))
- the inner closure escapes the scope of x
(by being returned from the outer closure).
If the closure is turned into a trait object (e.g. Box<dyn Fn(...) -> _>
) in the scope where it is created in (with all the captures in scope), we can't change anything there, since it'd require lifetime analysis.
But since the stabilization of impl Trait
, it's becoming increasingly common to return various types that contain closures (such as iterators), and that case can be readily served by this change, e.g.:
fn compose<A, B, C>(
f: impl Fn(A) -> B,
g: impl Fn(B) -> C,
) -> impl Fn(A) -> C {
// Currently requires `move`, but we can solve that.
/*move*/ |x| g(f(x))
}
There's only one issue I could think of, with this approach: Copy
by-value captures can have surprising outcomes, in that the capture leaves the original accessible, without a syntactical indication, e.g.:
let outer_get_x = {
let mut x = 0;
let get_x = || x;
x += 1;
get_x
};
assert_eq!(outer_get_x(), 0);
If x
were to be declared before outer_get_x
, then this code would not compile, as the closure would be borrowing it (which would conflict with x += 1
mutating x
), instead of holding onto a copy of it.
What can we do? I think that ideally we'd treat the closure as capturing by borrow, but only in terms of preventing mutable access to x
for the remainder of its scope, and otherwise capture a copy.
(Then you could only observe a problem if you had a copyable type with interior mutability.)
But I'm not sure what the best implementation approach for that would be.