On the topic of inferring `move` closures

One frequently requested feature is to infer the move keyword in closures. This is not always possible – to infer it with pure fidelity would require knowledge of the complete set of region constraints, and we can’t know the complete set of region constraints until we decide whether a closure is escaping (move) or by-reference. However, in some circumstances, notably when we see a 'static constraint, we could infer that move is required. (Note though that the move keyword is currently required more often than it ought to be. Once PR 21604 lands, the move keyword should only be required for closures that actually escape the current stack frame, either because they serve as a thread body or because they are returned to a caller.)

This would be simple to implement, but something about the idea of inferring move has always made me uncomfortable. This is because escaping closures feel like a different semantic entity to me, so it seems strange that we should even want to infer it. Today I came up with a good example to highlight what I mean. Consider this closure, which increments the upvar i from the surrounding environment:

fn foo() {
    let mut i = 0;
    bar(|| i += 1);
    println!("i={}", i); // Prints what?
}

If this is an escaping closure, then what is happening is that the closure copies i into its environment and increments the result there. This means that the final line will always print 0, because the closure is operating on a separate copy of i. If this is a non-escaping closure, then the closure will mutate i from its surrounding environment, and hence the final line will print 1 (assuming the closure is called once).

Now, in the compiler as it exists today, this distinction is syntactically clear. There is no keyword move, so the closure above is a non-escaping closure, and hence we expect it to mutate i in place. However, if we start inferring the move keyword, then the example above is ambiguous, and we would have to look at the definition of bar to determine what is happening – moreover, it may not be entirely clear, because the inference will be something of a heuristic. The inference I proposed (using 'static) is backwards compatible in that it doesn’t cause any program that currently executes to behave differently. But it will cause programs that would fail to compile today, because they need a move, to compile – and in so doing it may change the semantics from what you expect.

I’m not sure what’s the best way to proceed here: implement the inference, or close #18799 as “WON’T FIX”.

Seems like a real nasty gotcha for a teensy bit of concision. Not worth it to me.

+1 to closing as wontfix from me. I agree that escaping closures are semantically different than non-escaping ones and should use different syntax (when we had boxed closures, the syntax was clearly different as well: proc() vs ||). I feel that if the inference is implemented, I’m going to have to do extra non-trivial work to figure out whether a closure is escaping or not. I don’t think it’s worth to reduce readability to avoid typing a few characters.

I think move should be explicit, especially given the example above. Much like in C++, I definitely always want to know if something is being captured by copy/move/reference just by looking at the lambda/closure.

+1 Explicit. So far borrowing has been explicit, and this is quite analogous.

I agree with others here: move inference appears to be more trouble than it’s worth.

In terms of reasoning about code, the other form of capture inference for closures – inferring the kind of capture for each upvar based on usage – seems more in line with how you usually understand the ownership semantics of code. For example, inferring that a variable must be moved because the code in the closure consumes it (e.g. by calling mem::drop) feels like a natural extension of reasoning about ownership transfer. It’s also local, in that it only concerns the closure body.

Inferring move, on the other hand, is nonlocal (since it involves the way the closure is ultimately used), heuristic, and does not follow the usual patterns of ownership reasoning. As @Ericson2314 said, in general we’ve worked hard to keep ownership and borrowing fairly explicit with very clear exceptions (method receivers), and this seems to significantly muddle the story for closures.

I do think we should consider a form of this inference for generating error messages for cases where you need to insert move.

WONTFIX

I’ve been working a lot with futures & concurrency primitives. I end up having move everywhere in my code. Would it be possible to add some lighter syntax for indicating a move then?

I don’t think it’s possible to make the syntax shorter than a keyword without having the “ungoogleable” problem.

Well, closure syntax (||) itself is ungoogleable too.

Since it's backwards compatible I'm in favour of not implementing it now. If we find it too annoying we can implement it for Rust 1.x.

I've been trying to explain closures in detail recently, and it's hard enough to explain how move/no-move interacts with the which trait is implemented for the closure type, i.e. the &mut:/&:/: syntax we have at the moment (they don't interact, but I have found that people find the idea that they're essentially independent unintuitive). Adding an implicit dimension to this make the story slightly less clear (it adds another thing that influences how closures behave, also basically independent of the trait), at the moment it is just "no move is by reference, move is by value" which is nice.

FWIW, it's not entirely ungoogleable: when I search for "rustlang pipe syntax" the second result is the reference, with the quoted snippet there including:

...... A lambda expression is a pipe-symbol-delimited ( | ) list of identifiers followed by an expression.

Just FYI, this is not entirely accurate, or at least it won't be once PR https://github.com/rust-lang/rust/pull/21604 lands. I'm investigating how far we can go on removing the : syntax btw. Got some pans on the fire there, not yet sure how the stew will turn out.

I would agree that explicit seems better, in this case.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.