Uh oh!
There was an error while loading. Please reload this page.
unboxed closures - #77
Conversation
pcwalton
commented
May 16, 2014
+1, I think we want something like this. I'm gathering some statistics now that will better inform our decision. |
sfackler
commented
May 16, 2014
My gut says we'd really want some form of capture lists to handle the by value vs by ref issue. I'm not sure how often it'll actually be necessary to manually specify a by-ref capture though, so maybe we can revisit later if it becomes a problem. |
thestinger
commented
May 16, 2014
I struggled to come up with an unambiguous way of defining capture lists, and I think any approach would be very ugly with the current closure syntax. Since references are just regular values, you can capture by-reference without using capture sugar at all, so I don't think a capture list would make things significantly clearer. The closures proposed here are purely sugar for simple cases, so it should be as painless as possible to write them. An explicit It's not perfect, but In C++, capture lists are the defining characteristic of the closure syntax, because the simplest closure ( Capture lists may be intended to make capturing many values more clear, but I find that I always stop listing out the captures by hand where there are more than 2 because it's less painful to just capture anything that's referenced. I think there's a fine line between a helpful feature (dare I say |
There was a problem hiding this comment.
Theoretically, couldn't this be done without unboxed trait objects? A function that returns a closure isn't referring to an unboxed trait object but instead referring to a specific anonymous struct. The function's implementation would then use this anonymous struct as the type for whatever closure is actually returned. The two big downsides I could see are:
- You can't conditionally pick from several closures to return, and doing so would probably cause a very confusing error (because only one can actually be the correct type).
- I'm not sure how this would work if the function is generic. Does the anonymous struct then become generic too, using the same type parameters as the function? What if the function isn't generic, but it's part of a generic impl? This seems potentially problematic.
I'm not suggesting this is the way to go. Unboxed trait objects seem like a better solution. I just wanted to mention it as a potential alternative.
There was a problem hiding this comment.
I'm just using unboxed trait objects as jargon to refer to an anonymous type implementing some trait. Implementing it with sane error messages seems like it would go a long way to implementing this as a general purpose feature.
There was a problem hiding this comment.
I was considering my suggestion to actually be the inverse of rust-lang/rust#11455. In that proposal, a function returning an unboxed Iterator would actually return the concrete type of whatever iterator the implementation returns. I was thinking of this as the opposite, the return type would define the anonymous type and the closure in the implementation would be forced to be that same type.
But it turns out that's rather unnecessary (and overly complicated, because the function doesn't know what upvars there will be so it can't actually fully define the type). Because treating it the exact same way that the Iterator case is works just as well. So forget I suggested anything.
Valloric
commented
May 16, 2014
+1 on the general idea; the by-ref capture syntax is awkward, but no better approach is readily apparent. |
pcwalton
commented
May 16, 2014
Here are some statistics on closures (click through the charts at the bottom to see charts): https://docs.google.com/spreadsheets/d/1AMNPHhUxa7HKnoPAUD0Lj_fHxsScEa9rfWgK3JLYSjM/edit?usp=sharing |
pcwalton
commented
May 16, 2014
Out of 6,145 closures, 4,654 (75.7%) contain no or only immutable, copyable borrows, and would therefore have identical semantics if upvars were moved by default (excluding Most of the rest (19.2% of the total, 79.0% of the rest) contain at least one unique-immutable borrow. I have not yet analyzed for how many of them those uniquely-immutably-borrowed variables are used after the construction of the closure. |
lilyball
commented
May 16, 2014
Overall I'm very strongly in favor of this proposal. I was initially very against the One question I have about this is the traits for
|
thestinger
commented
May 16, 2014
Yes, I want it to be possible for user-defined types to implement the trait(s) via this sugar.
If it has the same number of an arguments, it will report an error for a duplicate implementation of the trait. If it has a different number of arguments, it will report an error when you attempt to call it. This is already implemented as an error message when two traits are in scope implementing a method with the same name, and using a different number of arguments can be considered an implementation of a separate trait. If variadic generics ever exist, it could be mapped to a single trait rather than multiple traits but it's not necessary and the sugar would still be how it's used, so it would only influence the error messages. |
lilyball
commented
May 16, 2014
What about the same number of arguments, but different types? In any case, it sounds like you're saying "it would be ambiguous", which is what I wanted to hear. |
thestinger
commented
May 16, 2014
@kballard: If it's the same number of arguments with different types, the compiler would reject it as a duplicate implementation of the trait via the coherence rules. |
huonw
commented
May 16, 2014
@pcwalton what is a "unique-immutable" capture? |
thestinger
commented
May 16, 2014
I took that to mean a capture of an affine (non- |
huonw
commented
May 16, 2014
Wouldn't that be covered by "immutable, noncopyable" and "mutable, noncopyable"? |
thestinger
commented
May 16, 2014
Ah, then I don't know what it is either. |
bstrie
commented
May 16, 2014
I love love love that this proposal avoids forcing us to re-introduce capture clauses. It's important to me that lambda syntax be very lightweight for the common case, a property that mandatory capture clauses would obliterate. @pcwalton, what code did you analyze to produce those numbers, and what tool did you use to generate them? |
huonw
commented
May 16, 2014
Random thought: we can possibly support C++14 generic lambdas relatively naturally. We currently have the I'm mainly writing this here so that it's recorded somewhere, and because it seems reasonable to avoid closing off this path, if possible. (If there is interest in discussing this outside of what's relevant to this RFC I'll make a post on /r/rust or the mailing list, to avoid this RFC being sidetracked.) |
pcwalton
commented
May 16, 2014
@thestinger@huonw Unique-immutable means that
@bstrie I'm a bit concerned about is having to write |
thestinger
commented
May 16, 2014
@pcwalton: I don't understand when moving or re-borrowing wouldn't be enough when it's being captured by-value. I think using |
pcwalton
commented
May 16, 2014
Yes, Niko told me about the reborrowing option for On Friday, May 16, 2014, Daniel Micay notifications@github.com wrote:
|
bstrie
commented
May 16, 2014
Can someone elaborate on reborrowing in this context? |
thestinger
commented
May 16, 2014
|
lilyball
commented
May 16, 2014
@pcwalton My understanding is that a |
lilyball
commented
May 16, 2014
Ok, so it sounds like this sugar is mapping to an infinite series of trait<R> Callable0<R> {fncall(&mutself) -> R;}
trait<R,A1> Callable1<R,A1> {fncall(&mutself,A1) -> R;}
trait<R,A1,A2> Callable2<R,A1,A2> {fncall(&mutself,A1,A2) -> R;}// etcwhich makes sense. I was interpreting each |
bstrie
commented
May 16, 2014
Looking at the categories of captures in pcwalton's spreadsheet, can someone confirm how each would map to the capture syntax proposed here? My best intuition: Also, are the parens in the latter case mandatory for disambiguation? |
lilyball
commented
May 16, 2014
Correct. Or if the value is sufficiently large, you can use If moving is acceptable, then that's correct. Otherwise, you can use
Yeah. Or you can just use
I believe it's unambiguous without them, but I also think it's better code style to use them. So I would imagine they wouldn't actually be required by the parser, but we'd still use them in code examples. |
lilyball
commented
May 16, 2014
For reference, if you want to reproduce the exact same behavior we have today, then you'd use either |
thestinger
commented
May 16, 2014
The parentheses wouldn't always be required, I just used them because my example wasn't as trivial as |
bstrie
commented
May 16, 2014
@kballard, thanks. I was just trying to get an intuition for how many upvars would opt out of by-value capturing, though since it's not really a 1:1 mapping I suppose it doesn't really answer my question (but is informative regardless). |
bstrie
commented
May 16, 2014
One more clarification regarding the syntax: presumably, after capturing by reference, you would not require the capture syntax on subsequent uses of the variable? i.e. letmut v = Vec::new();let c = || {(ref mut v).push(3); v.push(6);}...instead of: letmut v = Vec::new();let c = || {(ref mut v).push(3);(ref mut v).push(6);} |
thestinger
commented
May 16, 2014
@bstrie: You would need to repeat |
bstrie
commented
May 16, 2014
I can live with repeating the capture type. More verbose, but less magical, so it's a wash. And |
alexcrichton
commented
May 16, 2014
lilyball
commented
May 16, 2014
I prefer repeating. It allows expressing both a by-value and a by-ref capture in the same closure, which is theoretically useful (well, by-mut-ref is more theoretically useful in this scenario).
|
nrc
commented
May 17, 2014
I broadly like the proposal and agree capture clauses are pretty ugly. However, I really dislike annotating actual variables with properties of the formals, even if the formals are implicit. This is a big change from anything currently in Rust or (afaik) any other mainstream programming language. It seems confusing as well as not scaling beyond the smallest closures. There was a suggestion floating around (I think Niko's) that we make closures capture all arguments by value by default, we have some sugary syntax for a closure which captures all its arguments by reference (of course the defaults could be reversed if desired), and for the remaining case we use the by-value form and you have to explicitly create references in the caller or to use capture clauses. I'm not sure about the details of that, but it sits much better with me. |
thestinger
commented
May 17, 2014
I think large closures venture outside of the use case for closure syntax. The use case for this syntax is defining small callable objects with state inline with the body of another function. If it's a large amount of code, then it makes more sense to define it outside of the function as a
This will lead to performing unwanted shallow copies (potentially large ones) or unwanted by-reference captures simply because it's the only clean way to do it. It will encourage writing inefficient code. The use of manual by-reference captures via |
thestinger
commented
May 17, 2014
You can also do |
bstrie
commented
May 17, 2014
I agree with @thestinger, sprawling closures are an anti-pattern and should be discouraged.
My mental model of closures doesn't consider upvars to be formal parameters, and I'm willing to venture this is how most other people conceptualize it as well. Capture clauses themselves are already far removed from most programming languages (aside from C++, I can't think of any other language, niche or mainstream, that has syntax to allow you to determine the manner in which upvars are captured).
I would like to read this proposal, but at first blush it seems too coarse-grained to force a choice between 1) every upvar by-val, 2) every upvar by-ref, 3) a mixture by making manual bindings outside the function. pcwalton could weigh in here by giving us stats on what percentage of closures have at at least one by-val and one by-ref upvar. |
pcwalton
commented
May 17, 2014
I feel like we may not need any syntax for upvar captures. Fewer than 10% of closures in both Rust and Servo use anything other than the default, assuming we reborrow @thestinger was concerned about people accidentally capturing large structs by value, but I bet it's rare. I'll do some analysis to see how big the set of upvars is. |
alexcrichton
commented
May 17, 2014
Would the desugaring described allow for movement of owned captured variables into the closure? For example, today this compiles with an error: let a = box 1;(|| drop(a))()With this proposal, the closure would capture |
lilyball
commented
May 17, 2014
@alexcrichton I believe you are correct. You can move the It doesn't make sense to be able to |
huonw
commented
May 17, 2014
Unboxed closures can be run-once, if the "callable" trait they implement takes (The |
There was a problem hiding this comment.
Is this really different enough that it deserves to be deferred for a future RFC? Because if you're just looking to avoid a syntax bikeshed...
spawn(proc(x){ ... });// oldspawn(once |x| { ...});// new...then IMO this looks like it would be an improvement. Still not quite as pretty as the old do spawn, but I was never comfortable with how non-closurelike the proc syntax looks. :P
lilyball
commented
May 17, 2014
@huonw Ok sure, but the run-once variant uses a by-value |
huonw
commented
May 17, 2014
@kballard ... that's exactly my point. Generalised closures aren't forced to have |
pcwalton
commented
May 18, 2014
I did some analysis on the size of upvars being captured. Results are here: https://docs.google.com/spreadsheets/d/1AMNPHhUxa7HKnoPAUD0Lj_fHxsScEa9rfWgK3JLYSjM/edit?usp=sharing The tl;dr is that, on a 64-bit machine in the Rust compiler and standard libraries, 95.5% of upvars are 16 bytes or fewer, 97.2% of upvars are 32 bytes or fewer, and 98.2% of upvars are 64 bytes or fewer. |
huonw
commented
May 18, 2014
Did you calculate those percentages correctly? That data gives me even better results:
|
thestinger
commented
May 18, 2014
@alexcrichton: You can move into the closure, but it's not possible to move out via |
Kimundi
commented
May 18, 2014
I like this proposal, for all the reasons @thestinger has mentioned. In Rust, working by-value is simply the most composable option, and with the optional |
thestinger
commented
May 21, 2014
This is now updated to treat |
gereeter
commented
May 29, 2014
One alternative to the proposed by-reference capture system (the size of rust-lang/rust#14501 implies that this should not be thrown out) would be replacing
Downsides:
|
Kimundi
commented
May 31, 2014
@gereeter: That would make the reference operators have different behavior depending on the context where they are written, which apart from being confusing would also prevent the user from being able to choose whether they want to capture by reference, or to capture by value and take a reference to that. The advantage of the |
alexcrichton
commented
Jun 12, 2014
Closing in favor of the most recent unboxed closures RFC #114 |
Drastically simplify with ideas from tokio
No description provided.