-
Notifications
You must be signed in to change notification settings - Fork 1.7k
RFC: Filling in the details around unboxed closures #97
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| - Start Date: 2014-05-28 | ||
| - RFC PR #: (leave this empty) | ||
| - Rust Issue #: (leave this empty) | ||
|
|
||
| # Summary | ||
|
|
||
| Unboxed closures should be implemented with three traits (`Fn`, `FnMut`, and `FnOnce`), and there should be a leading sigil (`&:`/`&mut:`/`:`) before the argument list so the programmer can describe which one is meant. | ||
|
|
||
| # Motivation | ||
|
|
||
| This RFC simply addresses some points that were not ironed out in the previous unboxed closure RFC. | ||
|
|
||
| # Detailed design | ||
|
|
||
| This builds on RFC #77 "unboxed closures"; see the design for that. | ||
|
|
||
| There should be three traits as lang items: | ||
|
|
||
| #[lang="fn"] | ||
| pub trait Fn<A,R> { | ||
| fn call_fn(&self, args: A) -> R; | ||
| } | ||
|
|
||
| #[lang="fn_mut"] | ||
| pub trait FnMut<A,R> { | ||
| fn call(&mut self, args: A) -> R; | ||
| } | ||
|
|
||
| #[lang="fn_once"] | ||
| pub trait FnOnce<A,R> { | ||
| fn call_once(self, args: A) -> R; | ||
| } | ||
|
|
||
| The unboxed closure literal form `|a, b| a + b` creates an anonymous structure implementing one of the above three traits. Accordingly, we introduce new syntaxes for unboxed closures to correspond to the three traits above: | ||
|
|
||
| let f: |&: a, b| a + b; // implements `Fn` | ||
| let g: |&mut: a, b| a + b; // implements `FnMut` | ||
| let h: |: a, b| a + b; // implements `FnOnce` | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to suggest using this sugar at the type level as well. Personally, I find
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @anasazi I agree, |
||
|
|
||
| Once boxed closures are removed, the regular `|a, b| a + b` syntax will be an alias for `|&mut: a, b| a + b`, since that is the commonest trait to implement. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you have any measurements to reinforce this claim? Your datasets in the prior RFC have spoiled us. :) |
||
|
|
||
| The idea behind the syntax is that what goes before the `:` mirrors what goes before `self` in the `call`/`call_fn`/`call_once` function signature. This syntax avoids introducing any new keywords to the language. | ||
|
|
||
| The call operator `x(y, z)` will desugar to one of `x.Fn::call_fn((y, z))`, `x.FnMut::call((y, z))`, and `x.FnOnce::call_once((y, z))`, depending on the trait that `x` implements. If `x` implements more than one of `Fn`/`FnMut`/`FnOnce`, then the compiler reports an error and the `x(y, z)` form cannot be used. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that only one trait can be implemented, is there a reason to have separate names
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I interpreted this paragraph as saying that the compiler will report an error on the use of the call operator In any case, I see value in using distinct names for the methods. The only reason I could see not to do so would be to ease designing certain macros, if our macros were able to expand into method-items (which I believe they currently cannot). |
||
|
|
||
| We will remove `proc(A...) -> R` and replace with `Box<FnOnce<(A...),R>>`. | ||
|
|
||
| # Drawbacks | ||
|
|
||
| * The syntax may be ugly. | ||
|
|
||
| * It may be that `Fn` and `FnOnce` are too much complexity. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know about
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep in mind that |
||
|
|
||
| * Tupling the arguments may have ABI impacts, although I researched this on ARM-EABI and x86 and did not find any. | ||
|
|
||
| * Because of argument tupling, we lose the ability to pass DSTs by value, which has been proposed in the past. | ||
|
|
||
| # Alternatives | ||
|
|
||
| The impact of not doing this at all is that the precise trait that unboxed closures implement will be undefined, and we will continue to have `proc`. | ||
|
|
||
| An alternative to tupling arguments is to introduce variadic generics, but that seems like a lot of complexity. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #77's proposed trait implementation looked like impl |int, int| -> int for Foo {
fn call(&mut self, a: int, b: int) -> int { ... }
}It wasn't clearly stated, but the idea here is that this is effectively sugar for a trait like The implication here is that the function calling traits are actually a family of traits synthesized by the compiler (one for each argument count), rather than 3 specific lang item traits defined in libcore. Despite being very unconventional, this approach allows for omitting the tupling, and would not prevent passing DSTs by value. Has this approach been seriously considered and rejected?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A family of traits will cause weirdness around name resolution and also complicates the implementation and semantics considerably. I would much rather see if tupling causes any actual problems.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that tupling is conceptually much nicer. I brought this up largely because of the DST by-value issue, although I don't actually know how passing DST by-value would even work (I have to assume it would really be a fat pointer under the hood, but then why not just pass
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Variadic generics would allow passing DSTs by-value, yes? Assuming a hypothetical future Rust, this could justify changing the behind-the-scenes implementation. Would such a change be backwards-compatible, or would the details of argument tupling leak out anywhere?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There wouldn't be a family of traits. You would have the same traits, but variadic under the hood - you wouldn't be able to define and name them, but the closure type grammar is enough for referring to them. Tupling is a serious issue, you need devirtualization, inlining and SROA to happen in order to remove the overhead, so any remaining virtual closure calls (or even unboxed closure calls that don't inline) will have to read all their arguments from behind a pointer, instead of registers or direct stack offsets. Right now this also causes the loss of argument attributes, so it would prevent certain optimizations in all closures, before we get TBAA.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like we could have the calling convention handle
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eddyb That's not how calling conventions work. On all ABIs we care about, the calling conventions allow struct components to be passed in registers. Also, I don't see what you mean by devirtualization being necessary. This proposal reduces the need for devirtualization. The LLVM attribute issue is a valid one, but this should be fixed in LLVM. The logical conclusion of working around this limitation in the language design leads to absurdities such as changing APIs that would take Points to take two values, removing OO-style "self" structs, and whatnot. Actually, come to think of it, I could do the untupling at the caller side rather than the callee side. This means that in, most cases, the tupling need not be done at all, as there is no reason do it if calling the closure with the |
||
|
|
||
| # Unresolved questions | ||
|
|
||
| It remains to be seen how this interacts with not being able to use "for-all" quantifiers in trait objects. This will break some code until/unless we introduce this capability. How much is unknown. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this mean exactly?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can't use a trait as a region binding site. |
||
|
|
||
| ABI issues relating to tupling struct arguments on uncommon architectures like MIPS and non-EABI ARM have been inadequately explored. | ||
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.
Since these are builtin/generated by the compiler, is there any particular reason to not just use
callandcall_mut(which are more consistent).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.
Would these autogenerated function names ever appear in error messages, or otherwise be visible to the user?