-
Notifications
You must be signed in to change notification settings - Fork 1.6k
RFC: impl trait expressions #2604
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
Conversation
though having to explicitly declare a type in these situations can be | ||
unnecessarily painful and noisy. Closures are a good example of how | ||
this problem can be ameliorated by adding the ability to declare once-off | ||
values of anonymous types. |
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.
The motivation feels a bit thin; it would be good to work in some real world use cases of where it would be beneficial; I'm sure you won't have any problems doing that.
let foo = move || println!("{}", y); | ||
``` | ||
|
||
With this RFC, the above code becomes syntax sugar for: |
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.
I don't think calling it sugar does it justice; in particular, if you add type parameters to a closure, there's type inference going on to infer what the type of Self::Output
is as well as what Args
is.
}; | ||
``` | ||
|
||
Which, in turn, is syntax sugar for: |
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.
Which, in turn, is syntax sugar for: | |
Which, in turn, is syntactic sugar for: |
let foo = move || println!("{}", y); | ||
``` | ||
|
||
With this RFC, the above code becomes syntax sugar for: |
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.
With this RFC, the above code becomes syntax sugar for: | |
With this RFC, the above code becomes syntactic sugar for: |
|
||
This feature is fully described in the guide-level explanation. As this is a | ||
generalisation of the existing closure syntax I suspect that the implementation | ||
would be fairly straight-forward. |
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.
I'd like at least the following to be discussed:
-
What is the interaction with Closures Capture Disjoint Fields #2229?
-
What changes are there to the grammar? and are there any ambiguities (there are, see below)
-
What happens when I implement the
Fn
trait and it is a subtrait once removed ofFnOnce
? Do I also get the supertrait impls? -
How do I implement several traits at once? Can I do that? This ties into 3.
-
What is the type of an anonymous impl? A voldemort type?
-
Will
Copy
andClone
be implemented for the impl trait expression if possible like for closures? -
What happens if I write
let obj = impl Foo<'_> { ... };
? Normally this would quantify a lifetime'a
so we'd haveimpl<'a> Foo<'a> for X { ... }
... -
What rules if any apply for turning
let x = impl Foo { ... }
into a trait object?
This feature is fully described in the guide-level explanation. As this is a | ||
generalisation of the existing closure syntax I suspect that the implementation | ||
would be fairly straight-forward. | ||
|
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.
As for the ambiguity aforementioned, consider:
fn foo() {
struct X;
impl X {
fn foo() {}
};
}
This compiles today and impl X { ... }
is an item
followed by the empty statement ;
.
According to your RFC however impl X { ... }
is an expression which is made into a statement by the following ;
.
This is not an insurmountable challenge but you'll need to think about how to deal with it.
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.
cc @petrochenkov @eddyb @qmx ^ ideas?
# Prior art | ||
[prior-art]: #prior-art | ||
|
||
Other than closures I'm not aware of any prior art. |
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.
Some prior art to consider:
- Java's anonymous classes
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.
Kotlin and C++ also have a similar feature to Java's.
It's too bad this never came up before |
My version of this general idea (predating fn main() {
let world = "world";
let says_hello_world = struct {
world,
} impl fmt::Display {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "hello {}", self.world)
}
};
println!("{}", says_hello_world);
} The struct {
x,
y,
} impl Default {
fn default() -> Self {
Self { x: 1, y: -1 }
}
} impl Clone { // imagine `#[derive(Clone)]` generating this:
fn clone(&self) -> Self {
Self {
x: self.x.clone(),
y: self.y.clone(),
}
}
} |
I could be persuaded to be in favor of this, but my inclination is against it just because of Java. Anonymous classes in Java are huge source of noise and hard-to-understand code IMHO. In addition, I would like to see the following addressed in the RFC:
|
@mark-i-m I'd expect the answers to both of those questions to be identical to the answers for closures. |
|
||
```rust | ||
let y = String::from("hello"); | ||
let foo = move impl FnOnce<()> { |
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.
Mind that the type parameters of the Fn
traits are not a stable part of Rust, so be careful not to imply that this should work
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.
This should probably be written as impl FnOnce()
(cc @nikomatsakis)
Something like this which allows omitting the types of the closed over locals could be huge for macros that need to codegen implementors of a trait. Then again, the feature might also even eliminate the need for most such macros! |
let foo = move impl FnOnce<()> { | ||
type Output = (); | ||
|
||
extern "rust-call" fn call_once(self, args: ()) { |
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.
Aaaa I keep being reminded I need to fix this (it should ideally be just fn call_once(self)
here).
Question: Does this work? const FOO: impl Foo = impl Foo { ... }; |
I believe the syntax by @eddyb makes more sense so long as you must still write out I prefer anonymous types being explicit though, so maybe some syntax involving You could name the
In this, all captures take the form We could also improve the mechanisms for creating such types from procedural macros, so like some macro driven
There is also the |
Co-Authored-By: canndrew <[email protected]>
Co-Authored-By: canndrew <[email protected]>
Yeah, this is basically exactly Java's anonymous inner classes. Somewhat amusing that for 20 years Java had closures but only using the verbose AIC syntax. Then they finally added a terse lambda syntax as a syntactic sugar (conceptually if not implementation-wise) . And now in Rust we first had lambdas and now entertain the thought of generalizing them :) |
It would be nice to see more examples where the trait is not one of the |
Rust closures admit no polymorphism, not even lifetime polymorphism. I suggested It's less common but you might want to tie a closure return lifetime to an argument lifetime too. At that point, you want full lifetime polymorphism for closures. I think this proposal addresses roughly the same problems as polymorphism for closures. It gains some ergonomics in type descriptions via existing traits, but only with a dramatic ergonomics sacrifice for instantiation. |
fn foo() {
rank_2(|x: &u8| -> &u8 { x });
}
fn rank_2(x: impl for<'a> Fn(&'a u8) -> &'a u8) {} |
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This line of discussion seems increasingly off-topic... |
I think this should support implementing multiple traits (even if they have items with the same signature), and also support deriving traits. |
How does this handle implementing a trait with supertraits?For example implementing |
I see very little value in this. The example in the RFC is trivial to rewrite as let world = "world";
let says_hello_world = {
struct SaysHello<'a>(&'a str);
impl<'a> fmt::Display for SaysHello<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "hello {}", self.0)
}
}
SaysHello(world)
};
println!("{}", says_hello_world); As others have already mentioned it, this is very similar to a lambda. Lambdas are great; however, the use-case of this one is so narrow and so specific that having to do the above transformation by hand once in a lifetime clearly doesn't justify the added language complexity. |
|
That's a moot point, you can use a tuple struct.
This is simply false. This would mean that Rust is simply a pain to write in, a viewpoint from which I beg to differ.
They are unstable. I would be highly in favor of stabilizing manual
It's narrow and specific because it replicates a small subset of functionality already offered by other parts of the language, but only for one concrete situation/style. I understand that there are situations that it might be useful. But "there are situations where it's useful" isn't at all sufficient justification for any feature to be added to the language.
In your example, this doesn't really require such |
I suppose "narrow and specific" might be strongly phrased @ExpHP but the syntax mentioned by @H2CO3 is not much more complex than the syntax proposed here, and the syntax proposed here costs "strangeness budget". We're talking about declaring arbitrary traits here so one should not expect dramatic simplifications. In particular you'll never navigate the orphan rules without those where clauses. In other words, I'd consider this RFC "narrow and specific" because many traits will remain painful to implement this way, so crate authors must trade their preferred trait structure for one that "lambdas well". I'd think the most useful approach would be giving proc macros the information they require to capture like closures do, maybe eventually even doing a demo implementation of closures via such proc macros. There are likely a small number of traits, or trait combinations, that benefit enormously form this, so their authors could designed finely tuned constructions, rather than hack up their traits to make using this RFC's approach less painful. Also, I'd think almost everyone would love to pass more type information to proc macros, although some complexity exist of course. |
Why don't we just make closures castable to trait implementations if the trait has exactly one method? use std::fmt;
fn main() {
let world = "world";
let says_hello_world = | f: &mut fmt::Formatter | write!(f, "hello {}", world);
println!("{}", &says_hello_world as &fmt::Display);
} |
That would be a nice extension, but both approaches are valuable to have. For example, Java adopted this behavior (lambda definitions of "functional interfaces") in version 8, and has additionally always had support for the style of anonymous interface implementation described in this RFC. There are some issues with the lambda approach around specifying which trait should be implemented that this doesn't have. |
That would be specified by the cast. I added an example to my previous comment. It feels for me like:
Therefore, having closures castable to trait implementations (that have exactly one method) would be enaugh. |
Opposed. This adds significant cognitive burden to readers and does not solve an important problem. The existing mechanism is a special case that should stay a special case, not be generalized. |
@rfcbot fcp postpone I personally am generally positive on this idea and I'd like to thank @canndrew for taking the time to write up and open it. Nonetheless, I am moving to postpone for the time being. Let me explain. First, why am I in favor of this idea? Well, because I've often found a desire for this feature. One example would be in Rayon, where we often have to make "one off structs" to implement the Why is the time not ripe? While the details of roadmap is still in play, I think it seems pretty clear that we are trending towards a "refine the language, finish up things in play" sort of year, and this seems like a clear expansion with insufficiently strong motivation. (Along those lines, there are some alternative, more limited approaches that we might also consider. For example, my Rayon use case above would be better served via generic closures, which seem like they have kind of zero extra "cognitive load". A more aggressive step might be to take the Java approach of permitting |
Team member @nikomatsakis has proposed to postpone this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
The final comment period, with a disposition to postpone, as per the review above, is now complete. By the power vested in me by Rust, I hereby postpone this RFC. |
The impl-tools crate now offers a rough alternative to the feature proposed by this RFC: use std::fmt;
fn main() {
let world = "world";
let says_hello_world = impl_tools::singleton! {
struct(&'static str = world);
impl fmt::Display for Self {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "hello {}", self.0)
}
}
};
assert_eq!(format!("{}", says_hello_world), "hello world");
} That is, a struct (tuple or regular) must be declared explicitly, but the struct's name and field types may be omitted. (Unspecified field types are emulated with generics, with all the limitations that implies: bounds must be specified explicitly, else they are practically useless. Thus an alternative to the above is |
Rendered view
This is an idea I've seen floating around for a while. I like it, so I decided to give it a proper RFC.
Summary: Add
impl Trait { ... }
expressions as a kind-of generalization of closure syntax.