-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow closure expressions to expand to a &
or &mut
temporary
#756
Conversation
I think it's not common enough to deserve a special case. If this RFC is accepted, then nothing will stop people from asking for more special cases. Why, for example, generic integer literals can't be a bit more generic and turn into references to integral types when required? Ultimately, I'd support a general auto-ref coercion |
Is there reading somewhere that explains why creating a trait object is recommended over creating a generic and statically dispatched parameter? Looked at the motivations to find these, do you mind expanding?
|
This question was raised by @japaric on rust-lang/rust#21699:
The answer here is already defined by the coercion semantics. In this case, you'd be coercing to the type fn foo<F:FnMut>(x: &mut F) and the caller wrote |
@sinistersnare I wouldn't go so far as to say that one is recommended over the other, but there are definitely specific scenarios where a trait object is preferred:
|
@petrochenkov I guess that's the question. It seems worthwhile to me to add sugar for specific cases. I am rather negative on a generic autoref expansion. I personally prefer to see where moves and borrows occur; I found generic autoref more confusing than helpful when we used to have it (well, we had a more limited form, auto-cross-borrow). However, in this case the closure expression is an rvalue, so this information is not particularly helpful or interesting. (That said, the only kind of autoref that I am strictly opposed to is automatic |
And at the same time most of them occur invisibly in method calls and operators, including the
Hmm, auto-ref for all rvalues.. Looks like a nice idea actually. |
I agree with @petrochenkov. If it's decided that it's a good idea to add auto-ref for closure rvalues, it'd probably be just as good to have auto-ref for all rvalues. |
On Tue, Feb 03, 2015 at 02:42:31PM -0800, Tim wrote:
I see this point of view, though I consider closure expressions |
I worry about the pedagogical consequences of having a distinction between lvalues and rvalues for the purposes of auto-ref. In general, the lvalue/rvalue distinction is a distinction that compiler writers care about, not one that ordinary programmers care about. (Yes, you have to know the difference on some level, but for most people the distinction is purely intuitive and not something that people have to consciously think about; it exists primarily so that the compiler will reject nonsensical assignments like |
I actually think its not that difficult to explain the difference between rvalues and lvalues in Rust, and it might even help pedagogically because they explain exactly when a move/copy happens, and when a temporary is created.
|
@pcwalton to be clear, though, autoref for all rvalues is not what is described in this RFC, which is rather more limited. As I wrote...somewhere, I don't really consider this a form of general autoref, but rather making the |
To rephrase, this proposal basically changes the explanation of
to:
|
Hmm, auto-ref for all literals, nice idea too :D |
My feeling is that, without a change like this (or the DST-by-value change mentioned in Alternatives), there will be a large ergonomic tax for using boxed closures. I believe that unboxed closures are already overused, which leads to significant code bloat and longer compilation times, and think we should try to adjust this balance. While I understand the consistency point some have raised, note that this is by no means a general coercion -- it is a relatively small modification to the Put differently, I think the argument shouldn't be about consistency so much as: does this negatively impact your ability to reason about real code? I am unable to come up with any examples where it does. As such, I'm in favor of this RFC as-is. |
Since 1.0 has already been released without this (and 1.1 and quite possibly 1.2 as well), I don't think the ergonomic gains are worth the extra complexity and inconsistencies are worth carrying around for ever more. Everywhere else a value is borrowed (even for temporary literals), it has to be explicit. I don't find having to add |
Hear ye, hear ye. This RFC was moved into Final Comment Period as of Wed, June 10th. (Sorry, forgot to add this notice!) UPDATE: Fixed date :) |
Today is Thursday June 11. |
DST by value would be great to have, but it seems somewhat orthogonal to this RFC. In particular, recursive functions that take closure arguments are still a pain to manage, even if no trait objects are involved at all. Consider a pattern like this (common enough in the compiler, at least): fn recursive<F:FnMut>(..., f: F) {
match ... {
Something(left, right) => { recursive(left, &mut f); recursive(right, &mut f); }
...
}
} This code will fail during monomorphization due to infinite expansion. The reason is that it gets called first with the original closure type (let's call it C). Then called with fn recursive<F:FnMut>(..., f: &mut F) {
match ... {
Something(left, right) => { recursive(left, f); recursive(right, f); }
...
}
} But now when I call fn recursive<F:FnMut>(..., f: F) {
recursive1(..., &mut f);
}
fn recursive1<F:FnMut>(..., f: &mut F) {
// as above, but call `recursive1`, not `recursive`
} |
This doesn't seem like a problem, to me. Edit: to explain a bit more, the first alternative seems very consistent with the rest rest of the language. Borrowing of function arguments is always explicit. If a function expects a The second alternative is a very common pattern for recursive algorithms: one often needs to perform some initial processing on a function's arguments before calling the actual recursive function, and I, personally, find myself doing this quite often in any language. Rust adds even more reasons to desire a wrapper, such as using |
I feel that @aturon summed up my opinion quite well on this RFC. I've been trying to somewhat aggressively use My opinion on closures is that they're fundamentally designed to be ergonomic. All closures we have today can be replaced with an I would personally feel that the drawbacks to this RFC are far outweighed by putting boxed closures on equal footing with unboxed closures, so I'm in favor. |
What makes this an annoyance? Specifically, what makes it more of an annoyance than everywhere else in the language where explicit borrows are required for literals and variables alike? In my mind, closures are (and should continue to be) a sugary literal for an anonymous struct created by the compiler. This means that borrowing should be treated exactly the same as for struct literals (requiring Ideally, I'd like to see passing a closure directly to a function and assigning it to a variable first work exactly the same in all cases. I realize this isn't 100% true, today, but at the very least we shouldn't be moving further from that goal. It seems like there are two main motivations wanting to take an Since adding this proposed sugar makes closures different from every other literal in the language, I don't think calling it an "ergonomic speed bump" is sufficient motivation. At the very least the RFC needs to explain why saying I also want to note that until we get by value DSTs, it is possible even today to avoid the code bloat of many instantiations without requiring the calling function to add @nikomatsakis As an aside, is there any reason let closure = |x| x*2;
do_something(&mut closure) can't me made to work with inference? It seems analogous to inferring the type of an integer literal or a vector based on its later usage. |
Eh, I think there's a clear difference between array literals and closures. Array literal syntax exists because they're a primitive type; without the syntax you can't make an array. Closure syntax only exists to make it easy to use the Fn* traits. I don't think its unreasonable for them to have special handling as described in this RFC. That being said, I also dont think it's unreasonable to hold off on this RFC for a while. It's a nice-to-have, and it may be that by-value DSTs eliminate a majority of the cases this would be useful. If there's no urgency, why not wait and see where the chips fall. |
The key difference here is that using a generic vs using a trait object has many compile-time implications, and using trait objects is currently hamstrung ergonomically by requiring Closures are already a special case for "expand into something magical" and as @aturon mentioned earlier I don't think that this is really adding any more magic than is already present. |
Again, how is this different from non-closure traits? Functions still have to decide whether to be generic or take a trait object, and if they choose the latter, anyone passing a variable or literal has to prepend
I very much disagree. One of the things I like about Rust closures is they are conceptually quite understandable and not particularly magical: each closure becomes an instance of an unnameable, compiler-defined struct type that implements one or more traits corresponding to the function call operator. You can even create your own structs that do the same thing (though I realize the exact syntax hasn't been stabilized, yet). All of the magic has to do with creating the struct: what data fields it has, whether they are values or references, which of the function traits it implements, etc. As I recall, most if not all of that is determined by the function body, what variables it uses, and how it uses them. Aside from the creation of the anonymous struct type, they currently behave just like struct literals, and I think that is important. Losing this for some limited ergonomic gains that will be made redundant by planned future features seems ridiculous. Especially since it is possible to achieve the same result (callers don't have to use |
I'm of two minds here. I hear and respect @rkjnsn's concern that having I guess the question is whether closures are different from other things, like arrays. I think that they are. It is great to understand that a closure can be desugared into a struct that implements a trait, but in terms of how they are used closures do not feel so very much like "data structures" but rather "control flow". This is why, for example, it makes sense to have closures default to by-ref and require the |
Hm... I think after consideration I'm in favor of this RFC, even though I'm generally not in favor of such sugar. Because you can already replicate this behavior by dispatching inside a thin wrapper (as @rkjnsn pointed out), and it wouldn't apply to closures assigned to a variable, there isn't a situation where this would actually make your code harder to reason about, IMO. |
Huge -1 for this, since this problem is already solved by rust-lang/rust#23895 (discussion here), which was, incidentally, proposed and implemented after this RFC was proposed. For example, this code from the RFC: fn do_something(closure: &mut FnMut(i32) -> i32) {
...
}
...
do_something(&mut |x| x*2); can be rewritten as: fn do_something<F: FnMut(i32) -> i32>(closure: F) {
...
}
...
do_something(|x| x*2); And for the case of recursive functions that take closures, I would say that explicitly writing out references and avoiding dynamic dispatch is the best, since you could accidentally change the function's asymptotic runtime otherwise. (with something similar to this) |
@theemathas Letting it be caller's choice to use a trait object or not is my favourite solution, but how do you make sure |
@bluss using type ascription, of course! E.g. |
I had a hunch that this PR was catering to an obsolete need, due to seeing only a couple occurrences of There are around 200 relevant closure expressions and almost 90 Maybe that's not enough reason to stop this PR, but it personally feels like a waste a time to cater for usecases that are going extinct, and it just adds up to technical debt. The age of indirection and virtual calls has passed, rejoice the victory of |
that function be generic over the closure type (e.g., `F` where | ||
`F:FnMut()`). For example, closure objects reduce code bloat, they | ||
work better with object safety restrictions, and they avoid infinite | ||
monomorphic expansion for recursion functions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my experience, if you can use a &Fn(X) -> Y
argument to avoid infinite regress in the signature for functions with recursively passed closures, you can normally use &F where F: Fn(X) -> Y
the same way.
@eddyb you could view that as evidence that the annoyingness of writing functions that accept @bluss note that this change is equally helpful if you write @theemathas can you elaborate a bit more on the example? I don't quite see how this is solving the problem yet. The "keep adding extra levels of indirection" problem is interesting, but strongly suggests that we should encourage people to handle recursive cases by pulling the reference into the fn signature, in which case this change makes that more ergonomic. All that said, I continue to rest on a knife's edge here. Clearly this change makes the expansion of |
I do want it to be more convenient and desirable to use virtual dispatch, since I believe there is great opportunity for profile-based tuning of function dispatch in Rust. This seems like it helps, but I haven't looked too closely. |
@brson yesterday on IRC I was mentioning adding static -> dynamic dispatch conversion to LLVM's mergefunc pass (so it can merge two functions that are identical except for some calls, which can be turned into indirect calls to a parameter of those functions). @cmr informed me that there's an associated cost to indirect calls, at least on older CPU microarchitectures, mostly due to the fact that a Now that you mention profile-guided optimizations, the kind of automatic virtualization I described seems perfect if the costs have been measured already. |
@eddyb I am somewhat skeptical that we ought to be automatically converting to static dispatch, but regardless we want users to be able to easily throw the switch themselves. |
@nikomatsakis What I was suggesting was optimizing away code bloat by turning static dispatch into dynamic dispatch, wherever there would be a measurable gain. That said, if do we get some sugar, I'd prefer being able to pass trait objects by value, instead. |
Per my last comment, I'm just going to withdraw this RFC for now. Perhaps we'll revisit this theme in the future after we've had more progress on closures in other areas. Thanks all for the comments and discussion! |
Modify the || expression sugar so that it can expand to either F, &F, or &mut F, where F is a fresh struct type implementing one of the Fn/FnMut/FnOnce traits.
Rendered view.