Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upRFC: Split the `||` unboxed closure form into two forms and remove the capture clauses and self type specifiers #231
Conversation
This comment has been minimized.
This comment has been minimized.
bgamari
commented
Sep 10, 2014
This comment has been minimized.
This comment has been minimized.
reem
commented
Sep 10, 2014
|
Is there a reason this distinction can't be made with |
This comment has been minimized.
This comment has been minimized.
|
The problem with |
This comment has been minimized.
This comment has been minimized.
|
@reem Also, |
This comment has been minimized.
This comment has been minimized.
|
@reem To clarify: the new semantics for |
huonw
reviewed
Sep 10, 2014
|
|
||
| Having to specify `ref` and the capture mode for each unboxed closure is inconvenient (see Rust PR rust-lang/rust#16610). It would be more convenient for the programmer if, the type of the closure and the modes of the upvars could be inferred. This also eliminates the "line-noise" syntaxes like `|&:|`, which are arguably unsightly. | ||
|
|
||
| Not all knobs can be removed, however: the programmer must manually specify whether each closure is escaping or nonescaping. To see this, observe that no sensible default for the closure `|| (*x).clone()` exists: if the function is nonescaping, it's a closure that can be called multiple times and returns a copy of `x` every time; if the function is escaping, it's a closure that can be called once and returns a copy of `x`. |
This comment has been minimized.
This comment has been minimized.
huonw
Sep 10, 2014
Member
I don't understand this? Why is it only once-callable if it escapes? It seems that the following works fine with our current closure scheme:
fn foo<'a, T: Clone>(x: &'a T) -> impl Fn<(), T> + 'a {
|&:| (*x).clone()
}(Using abstract return types & capture by value.)
This comment has been minimized.
This comment has been minimized.
huonw
Sep 10, 2014
Member
And, even something like
fn foo<T: Clone>(x: T) -> impl Fn<(), T> {
|&:| x.clone()
}can be called multiple times, despite moving x into the closure.
This comment has been minimized.
This comment has been minimized.
aturon
Sep 10, 2014
Member
@huonw I think this was a mistake in the RFC writeup. The intent was to allow escaping closures to infer the closure type.
In particular, although proc always moves in its upvars, it may still only use them by-ref, and hence can be a Fn.
This comment has been minimized.
This comment has been minimized.
reem
commented
Sep 10, 2014
|
I'm mostly just against using |
huonw
reviewed
Sep 10, 2014
|
|
||
| 1. Any variable which is closed over and borrowed mutably is by-reference and mutably borrowed. | ||
|
|
||
| 2. Any variable of a type that does not implement `Copy` which is moved within the closure is captured by value. |
This comment has been minimized.
This comment has been minimized.
huonw
Sep 10, 2014
Member
So, the only distinction between Copy and non-Copy types that are used by-value inside the closure is the size of the closure itself? (i.e. non-Copy types are stored inline in the closure environment but Copy types only have a reference.)
Is this a special case that's actually worth having? It seems like "anything used by-value is captured by value" isn't a crazy rule?
huonw
reviewed
Sep 10, 2014
|
|
||
| The `ref` prefix for unboxed closures is removed, since it is now essentially implied. | ||
|
|
||
| The `proc()` syntax is repurposed for unboxed closures. The value returned by a `proc()` implements `FnOnce`, `FnMut`, or `Fn`, as determined above; thus, for example, `proc(x: int, y) x + y` produces an unboxed closure that implements the `FnOnce(int, int) -> int` trait. Free variables referenced by a `proc` are always captured by value. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
pcwalton
Sep 10, 2014
Author
Contributor
All Fn implicitly also implements FnOnce (since if you can call something multiple times you can call it once).
This comment has been minimized.
This comment has been minimized.
huonw
Sep 10, 2014
Member
Right, so it seems like it's more correct to state "produces an unboxed that implements the Fn(int, int) -> int trait" since this is the most specific statement: every closure implements FnOnce, but only a limited subset implement FnMut and an even smaller subset of this implement Fn.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
@huonw Corrected example. |
This comment has been minimized.
This comment has been minimized.
SiegeLord
commented
Sep 10, 2014
|
I'd prefer different syntax for I don't understand how the trait bound syntax sugar can possibly work. Is it going to be To that end, I'd prefer to keep the current sugar for trait bounds. |
This comment has been minimized.
This comment has been minimized.
|
@SiegeLord The grammar will be I strongly prefer the path syntax, because |
This comment has been minimized.
This comment has been minimized.
|
Not only that, |
This comment has been minimized.
This comment has been minimized.
|
My first reaction was that I am aware that by-value by default and by-ref with sugar was attempted and later rejected. It would be interesting to know how many of the old let (tx, rx) = channel();
let data = arc.clone();
spawn(|| {
let tx = tx;
let data = data;
tx.send(data.crunch());
});What about macro_rules! take { ($($x:ident),+) => { $(let $x = $x;)* } }let (tx, rx) = channel();
let data = arc.clone();
spawn(|| {
take!(tx, data);
tx.send(data.crunch());
});Or let (tx, rx) = channel();
let data = arc.clone();
spawn(|| { take!(tx, data);
tx.send(data.crunch());
}); |
This comment has been minimized.
This comment has been minimized.
|
FWIW, it's already a lot of effort to capture (shared) things in a task-spawning closure, e.g. let data = Arc::new(...);
let flag = Arc::new(Mutex::new(...));
let (tx, rx) = channel();
for i in range(...) {
let data = data.clone();
let flag = flag.clone();
let tx = tx.clone();
spawn(|| {
// use data, flag, tx
})
}The (Also, fwiw, the macro would have to expand to a single let, via a tuple match like |
This comment has been minimized.
This comment has been minimized.
|
BTW, |
This comment has been minimized.
This comment has been minimized.
netvl
commented
Sep 10, 2014
|
Looks really great, though |x, y| x + yis non-escaping closure and |:x, y| x + yis escaping one. This syntax is neat and not alien like |
netvl
reviewed
Sep 10, 2014
|
|
||
| 2. Any variable of a type that does not implement `Copy` which is moved within the closure is captured by value. | ||
|
|
||
| 3. Any other variable which is closed over is by-reference and immutably borrowed. |
This comment has been minimized.
This comment has been minimized.
netvl
Sep 10, 2014
Does this mean (taking 1st and 2nd items into account) that Copy-types are always borrowed by reference? I think this is strange restriction, I certainly wouldn't expect such behavior. Everywhere else Copy types are always implicitly copied, as far as I know.
This comment has been minimized.
This comment has been minimized.
Valloric
commented
Sep 10, 2014
|
|
P1start
reviewed
Sep 10, 2014
|
|
||
| The `ref` prefix for unboxed closures is removed, since it is now essentially implied. | ||
|
|
||
| The `proc()` syntax is repurposed for unboxed closures. The value returned by a `proc()` implements `FnOnce`, `FnMut`, or `Fn`, as determined above; thus, for example, `proc(x: int, y) x + y` produces an unboxed closure that implements the `Fn(int, int) -> int` trait (and thus the `FnOnce(int, int) -> int` trait by inheritance). Free variables referenced by a `proc` are always captured by value. |
This comment has been minimized.
This comment has been minimized.
P1start
Sep 10, 2014
Contributor
and thus the
FnOnce(int, int) -> inttrait by inheritance
The traits don’t inherit each other at the moment, but it was discussed as an alternative in the unboxed closures RFC. Should the inheritance between traits be added to this RFC?
This comment has been minimized.
This comment has been minimized.
|
Another suggestion by @aturon is |
nrc
assigned
pcwalton
Sep 11, 2014
This comment has been minimized.
This comment has been minimized.
SiegeLord
commented
Sep 11, 2014
The RFC could be amended to allow that in the closure construction syntax, as a way to hint/override inference. This is not unprecedented (e.g. with number literals suffixes). Line-noise is the only self-consistent argument here, I think. Then again, I don't feel super-strongly as I despise the
That crossed my mind, but moving |
This comment has been minimized.
This comment has been minimized.
|
A move and copy are exactly the same thing under the hood (all by-value uses of a value are semantically a shallow byte-copy); the only difference is the source is disallowed from further use for non- |
This comment has been minimized.
This comment has been minimized.
SiegeLord
commented
Sep 11, 2014
|
The very reason why 'move' vs 'copy' distinction exists is to be able to use both terms without introducing special cases.
In writing that sentence you completely devalued the word 'move' as a term. If I can't use 'move' to unambiguously mean the move of ownership without qualifying whether I use a move move vs a copy move, why even introduce the distinction? |
This comment has been minimized.
This comment has been minimized.
|
I personally think we emphasise the difference far too much (and end up with unnecessary complications the other way), and people end up thinking at least one of an "implicit copy" or "move" is magical, but neither are and they're actually essentially identical. I try to use "by-value use" for the umbrella term but that is not appropriate for this closure syntax (e.g. In most cases, people don't care that a by-value use transfers ownership, other than the fact that the compiler complains. That is, transferring ownership is rarely an ends in itself, just a necessary property required for safety, hence there isn't actually that much value in having strict rules about not connecting "implicit copies" and ownership transfers (other than pedagogy, but we don't have good pedagogy around this right now anyway IMO).
Yes, that's my point: we make too big a deal out of the distinction. |
alexcrichton
force-pushed the
rust-lang:master
branch
from
6357402
to
e0acdf4
Sep 11, 2014
This comment has been minimized.
This comment has been minimized.
|
I like most of the ideas in here (which is not surprising...). After thinking about it a bit, the proposed capture mode rules for non-escaping closures also make sense to me. Would it be fair to summarize these as "every free variable is captured by reference, except when this would prevent the body of the closure from being legal, in which case the variable is captured by value instead"? (Like others, I'm not sure if it makes sense to consider On to important matters: bikeshedding the syntax of escaping closures! As there aren't many hard constraints in the current language, both of my considerations relate to compatibility and consistency with potential future language constructs. First of all, I think it would be nice at some point to grow support for unboxed trait object literals of any trait, not just Second, I think (Incidentally, if we already had |
This comment has been minimized.
This comment has been minimized.
|
I have a strong feeling this would lead to ambiguous/hard parsing issues, but I'll mention it anyway just in case:
Edit: maybe if you require writing |
aturon
force-pushed the
rust-lang:master
branch
from
4c0bebf
to
b1d1bfd
Sep 16, 2014
This comment has been minimized.
This comment has been minimized.
netvl
commented
Sep 17, 2014
|
Once again I want to suggest re-purposing |
P1start
reviewed
Sep 17, 2014
|
|
||
| Having to specify `ref` and the capture mode for each unboxed closure is inconvenient (see Rust PR rust-lang/rust#16610). It would be more convenient for the programmer if, the type of the closure and the modes of the upvars could be inferred. This also eliminates the "line-noise" syntaxes like `|&:|`, which are arguably unsightly. | ||
|
|
||
| Not all knobs can be removed, however: the programmer must manually specify whether each closure is escaping or nonescaping. To see this, observe that no sensible default for the closure `|| (*x).clone()` exists: if the function is nonescaping, it's a closure that returns a copy of `x` every time but does not move `x` into it; if the function is escaping, it's a that returns a copy of `x` and takes ownership of `x`. |
This comment has been minimized.
This comment has been minimized.
P1start
Sep 17, 2014
Contributor
Can this really not be inferred? With enough complexity in the compiler, it could default to always moving into closures unless the value moved into it is used after the closure is defined, in which case it could default to by-reference. Is the problem with this approach just that it would require an incredibly complex (and therefore probably buggy) implementation in the compiler, or is there another reason why this can’t be inferred?
This comment has been minimized.
This comment has been minimized.
reem
Sep 17, 2014
It can probably be inferred, but the complexity means that this would be a very complex source of bugs for users refactoring code - I think it's similar to why function types are not inferred.
This comment has been minimized.
This comment has been minimized.
P1start
Sep 17, 2014
Contributor
I thought about that, but I don’t think it’d be a source of bugs at all—it’d be almost entirely invisible to the user. From what I can tell, the non-escaping closures in the RFC are accepted in every place the escaping ones are. The only difference is how they do things. I am merely proposing that the non-escaping closures move whenever possible, and that the now-redundant escaping closures be removed.
I suppose the problem with this method is that a lot (most?) of the time you don’t want to move the values into the closure. Perhaps the proposed non-escaping closures could stay as-is, and the escaping ones could be removed completely. The only advantage I see in the escaping closures is that they move things by value more often, which often isn’t even an advantage. Choosing by-ref by default is the most extensible option, as one can still opt in to the old behaviour; e.g., || { let foo = x.clone(); drop(x); foo }. Perhaps I should just rethink the RFC’s proposed escaping closures as sugar for the above for when that functionality is needed.
pcwalton
merged commit 2e7023d
into
rust-lang:master
Sep 18, 2014
This comment has been minimized.
This comment has been minimized.
reem
commented
Sep 18, 2014
|
@pcwalton Are there meeting notes for the decision to merge this? Curious what was discussed. |
This comment has been minimized.
This comment has been minimized.
|
@reem I think these are the last meeting notes about this topic. |
This comment has been minimized.
This comment has been minimized.
The RFC was not updated to reflect this decision. |
pcwalton commentedSep 9, 2014
No description provided.