-
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
RFC: Finalize syntax and parameter scoping for impl Trait
, while expanding it to arguments
#1951
Conversation
One thing not addressed in the RFC is fn some_func(f: impl Fn(impl Debug) -> String) We've talked about wanting this case (and only this case) to have an existential or possibly higher-rank semantics, rather than universal - a sort of contravariance. Do you think that's not a good idea anymore, or is it just missing from the RFC? |
Thinking more zbout it now, though, I wonder if that's such a good idea. There are other traits for which that would be ideal - fn func(arg: impl From<impl ToString>) Since we can't make a correct determination of what you want in every case, leaving these all as universals (rather than a special case for fn some_func(f: impl for<T: Debug> Fn(T) -> String)
// maybe even
fn some_func(f: impl<T: Debug> Fn(T) -> String) |
text/0000-expand-impl-trait.md
Outdated
``` | ||
|
||
Here `impl Trait` is used for a type whose identity isn't important, where | ||
introducing an associated type is overkill. |
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.
If I understand correctly, this proposal would only allow returning impl Trait
when implementing a trait that uses this new syntax. This prevents the use of impl Trait
return types when implementing trait functions that return an associated type.
This means, for example, that the Service
trait from Tokio (link) would have to be changed to the following in order to support Service
s returning impl Future
s:
pub trait Service {
/// Requests handled by the service.
type Request;
/// Responses given by the service.
type Response;
/// Errors produced by the service.
type Error;
// NOTE: No more `type Future`
/// Process the request and return the response asynchronously.
fn call(&self, req: Self::Request) -> impl Future<Item = Self::Response, Error = Self::Error>;
}
But now because the Future
associated type has been removed, it's impossible to place bounds on the output of a Service
. For example, when writing a fn foo<S>(s: S) where S: Service
, it's no longer possible to constrain <S as Service>::Future: 'static + Sync + Clone + etc.
Similarly, for the Iterable
trait provided, it's impossible to write fn use_exact_size_iterable<I>(x: I) where I: Iterable, <I as Iterable>::Iterator: ExactSizeIterator
.
Because of these restrictions, library authors would be forced to choose whether they want their trait to be usable with impl Trait
, or whether they want their return types to be boundable. This would be a very difficult and conflicting decision, and either choice would undoubtedly result in frustration for users of the trait who either want to use impl Trait
or place bounds on return 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.
Its worth noting that there are impls that can't be written with the associated type form - any type parameterized by a closure, for example, because closures have anonymous types.
Likely we someday want to support something like Serivce::call::Return
as a type.
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.
@withoutboats Right-- it's clear that impl Trait
allows not only more convenient trait impls, but impls that weren't possible before. It seems odd to force trait-writers to choose to either allow those impls OR allow writing bounds-- neither is fully expressive.
I'd prefer the syntax for trait declaration go unchanged (making all current trait impls forwards-compatible with impl Trait
) and that users be able to specify associated types using something like the suggested "fully-explicit" syntax.
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 prefer the syntax for trait declaration go unchanged (making all current trait impls forwards-compatible with impl Trait) and that users be able to specify associated types using something like the suggested "fully-explicit" syntax.
Can you explain what you mean?
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.
WRT forwards-compatible traits: under this proposal, traits whose functions return associated types are not usable with impl Trait
return types (if I understand the proposal correctly). In order for a trait impl to use impl Trait
in the return type of a function, the trait function would need to be explicitly declared with an impl Trait
return type, rather than returning an associated type.
WRT my preference: leave trait declaration alone, and allow trait impls to use existentials as associated types. I'm not ready to propose my own syntax, but using the one suggested at the end of this RFC, it would look like this:
struct MyIter;
abstype MyItem: Fn(i32) -> i32;
impl Iterator for MyIter {
type Item = MyItem;
fn next(&mut self) -> Option<MyItem> {
Some(|x| x + 5)
}
}
Note that this impl would be impossible to write under the current proposal, as it requires the use of existential return types in traits that are already stabilized (and thus couldn't be changed to have their associated type removed).
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.
Quick note that foo::Return
is syntactically problematic, because Rust lets you define a value-like thing (such as a fn) and a type-like thing (including a module) with the same name. So this would be perfectly legal:
fn A() -> impl Trait { … }
mod A { pub type Return = ...; }
// what does A::Return refer to?
(Alternately the second A
could be a struct.)
One potential solution would be using a keyword, foo::return
, but that doesn't generalize if for any reason there's a need for additional pseudo-associated-types on fns.
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.
Quick note that foo::Return is syntactically problematic
Maybe we could implement typeof
and use <typeof(foo) as FnOnce>::Output
? (The typeof
keyword is already reserved.)
Is there a plan to support |
If you write this: trait Foo {
fn foo(&self) -> impl Trait;
} Is there then some way to use a (EDIT: I mean, because you can't write |
I think this is actually the most important question that should matter when deciding on Regarding dropping the keyword altogether – I also consider a current (bare |
To be clear, this situation is discussed in the RFC:
So basically, I still hold the view we discussed back at Rust Belt Rust, but wanted to leave it to a future RFC after we have more experience with full higher-ranked bounds (i.e. ones over types). |
In addition to my previous question, what about this: trait Foo {
fn foo(&self) -> impl Trait where Self: Sized;
} Does that make the "virtual associated type" disabled for |
@tomaka You're right that the implications for trait objects are under-specified here. I'll give it some deeper thought soon, but we should definitely strive to make the feature as compatible with trait objects as we can. |
Being able to disable some associated types when you use the trait as a trait object is definitely something I would love to have, but it's probably out of scope of this RFC. |
I think that @tomaka's point (about object types) and @cramertj's point (about compatibility with existing traits) are really the same point. I am inclined to agree with @cramertj that it might be nice to have the trait make the associated type explicit: trait Foo {
type Output: Debug;
fn blah(&self) -> Self::Output;
} while allowing the impl to use impl Foo for Blah {
fn blah(&self) -> impl Debug { ... }
} This would dovetail nicely with some kind of way to infer associated types based on the definitions of other items, which has long been requested (but possibly interacts with defaults). That seems like a mildly complex feature on its own though that merits some amount of thought (basically laying out the precise rules for when such inference occurs). |
I agree it is very important that we consider how understandable Rust will be with this feature.
I do not however fully agree with this point. I don't claim it's an open-and-shut case, but I think that I do think the choice of Also, I feel like the "plain English" intution isn't that strong for any/some. i.e., I could see someone describing I guess at the end of the day I feel like what is most important for teaching is the existence of an explicit syntax. That is, I think it's important to be able to "desugar" to a syntax that makes the universal/existential distinction explicit, but I don't think that the shorthand syntax has to embody that distinction. |
I consider this a good thing, because Similarity to
Now I see that. So there are basically two issues: (1) You can usually use some instead any in English (I somehow wasn't conciously aware of that, treating But still, I think these concepts may be worth differentiating syntactically. If not with these keywords, then maybe others. Although I still think that |
One hard part of teaching is relevance, that is, while many people do like knowing all of the details up front, many people only want to know whatever is relevant for what they're trying to do. To me, this is why the same syntax is important; I don't think many Rust programmers actually care about the difference here. We get people in |
I agree there is a distinction, but I think saying they are totally different is a stretch. There is a deep connection between "universal" and "existential" quantification -- it comes down to "who knows what the value is, and who doesn't". Put another way, no matter where it appears, an In argument position, the caller knows the real type, and the callee has to use the trait. In a very real sense, the hidden type in question is an input to the callee (just like the arguments). In return position, the callee knows the real type, and the caller has to use the trait. In a very real sense, the hidden type in question is an output from the callee (just like the return value). These don't seem so wildly different to me, which I think is why in OO-languages it feels so natural to have |
I was thinking about the decision to disallow fn foo<T, U>(x: T)
where T: Foo<U>, U: Bar, Trait matching doesn't have a natural variance (we make all trait matching invariant, for one thing), whereas arguments/return-values do. This makes me wonder if it makes sense to disallow "nested impl Trait" altogether for now (or maybe this is what the RFC text already said? I remember it as only disallow |
Thanks, I embarrassingly missed this. Leaving it disallowed seems fine to me! |
The only downside of this is that it doesn't work nicely with impl Traits capturing the type params in scope because associated types don't inherently. That is, this could not be trait Foo {
type Bar;
fn baz<T>(&self, arg: T) -> Self::Bar;
} |
Yes, a very good point. And of course extending to ATC quickly gets into higher-order unification / pattern-matching (which of course is also true of the explicit syntax). Seems like it really makes sense to dig more into those algorithms and try to understand the true limits. |
text/0000-expand-impl-trait.md
Outdated
correct choice of `some` or `any` seems like an unnecessary burden, especially | ||
if the choice is almost always dictated by the position. | ||
|
||
Pedagogically, if we have an explicit syntax, we retain the option of |
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.
Is it possible to use a more accessible word here?
text/0000-expand-impl-trait.md
Outdated
```rust | ||
trait Iterable { | ||
type Item; | ||
fn iter<'a>(&'a self) -> impl Iterator<&'a Item> + 'a |
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 be Self::Item
not Item
One thing that's unclear to me from the wording of this RFC is whether the signature I'm also curious how this could apply to something like Diesel where we have two "phases" of traits that are usually implemented. The first set applies to query construction, and handles things like type checking. The second set applies to actually executing the query, and making sure that it doesn't contain types or expressions specific to another backend. We could theoretically end up with a trait that looks like this: trait QueryDsl {
fn select<T>(self, selection: T) -> impl QueryDsl;
fn filter<T>(self, predicate: T) -> impl QueryDsl;
fn execute<Conn>(self, conn: &Conn) -> QueryResult<usize> where
Conn: Connection,
Self: QueryFragment<Conn::Backend>;
} However, I suspect that would mean that this code would not work: let conn = PgConnection::establish("...").unwrap();
users.select(id).execute(&conn).unwrap() Since that would knowing that the return type of |
re lifetimes: My understanding is that the proposal is to work exactly like trait objects today. |
I'm quite pleased with the fully explicit strawman :). Things like that make me less worried than I'd be otherwise. |
Even with Personally I think it's fine as long as nothing is inferred from the function body, and it is all based on type signatures that would be a breaking change to change anyways. Even with just the signature, and even having just two implicit traits, I could see this being a very confusing implied relationship. Kind of like how |
Send + Sync inherently introduce breakages because of their inferred nature. This is already true. The word We have rules that adding impls is not a breaking change, but this does not apply to impls of auto traits (positive or negative). The rules about what these impls mean are subtle and actually rather surprising. In other words, what the RFC has proposed is consistent with decision we have already made about auto traits: in order that types be
Well I'd strongly prefer never to stabilize the ability to define auto traits & to treat it essentially as a kind of lang item. Precisely because the concerns people are raising already apply in other contexts. Auto traits violate all of our rules. |
We've been moving for years in the other direction (they used to be called "type kinds"). |
That's not how I would interpret what's happened to |
This RFC talks mostly about functions, but constants may want both kinds of // Generic integer constants
const C1: impl __Integer__ = 10; // const C1<T: __Integer__>: T = 10; // "any"
// Closure constants
const C2: impl Fn() = || {}; // const C2: __UniqueClosureType__ = || {}; // "some" What the "post-rigorous" way to discern between them would be? |
@petrochenkov I believe const integers being any intereger will probably be a completely separate proposal, especially because it'd have to decide on what the 'any integer' type is. I'm not sure that would even want to use Edit (now that I'm on desktop): To clarify, I mean in
Since |
My mental model of |
I've rechecked, the RFC as merged introduces
I agree with this interpretation, but if it's followed, then the generic constant case (which is practically more important, IMO) is left without sugar, unless some new "some"/"any" separation is reintroduced, which this RFC tried to avoid. |
The RFC is already merged, but is there any chance we could rethink the decision to introduce I'd like to respond to some of the arguments provided in the RFC to explain why I dislike the idea of having Argument from learnability
I'm not sure about this. I actually don't see a reason why somebody would do that, because you don't want it to work with every type implementing
from TRPL, chapter about generics So it's also clearly about working with "multiple types". Using Now, let's assume it actually would make it a bit easier for a beginner.
Argument from ergonomicsThis one is basically about fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U> being more ergonomic than fn map<U>(self, f: impl FnOnce(T) -> U) -> Option<U> First of all, I feel this is a rather weak argument. While I agree that you get the meaning of Another point I'd like to raise here is that there is no comparison to using a where-clause: fn map<U, F>(self, f: F) -> Option<U>
where F: FnOnce(T) -> U fn map<U>(self, f: impl FnOnce(T) -> U) -> Option<U> It might be highly subjective, but I find the one with Argument from familiarity
I don't agree here. Consider the following: trait Foo {}
fn foo(v: Vec<impl Foo>) vs interface Foo {}
void foo(ArrayList<Foo> list) While the Rust example expects a I don't know how to reply to the other part of the argument, because I'm not sure how it is related to Stylistic overheadAs mentioned in the RFC, there was a point raised by @nrc about it being stylistic overhead / one more syntax to learn. Now, this is a rather weak argument on my side, but
I personally prefer the In the end, I'd like to rephrase your words that "it's not a new concept" but rather a new syntax for an existing concept. So it essentially boils down to the question whether or not the syntax is better and whether or not it's good to have 3 alternative syntaxes. For me, that's a clear no, given that I find it more confusing from the perspectives I explained above, namely
@aturon Please don't take this personal, you're doing great work! I just wanted to give my opinion here, as a Rust library developer. Also note that I don't know very much about type theory, so I could be missing substantial facts which invalidate some of my arguments; if that's the case, please tell me and I'll remove them. Thanks for reading! |
One other thing I find weird about the impl Trait in argument position syntax is the type parameters that are used without being declared anywhere. I think that would lead to confusion for beginners as well. |
I think there's some misunderstanding here: fn fn1() -> impl Debug { vec![0] }
fn fn2() -> Vec<i32> { vec![0] } The first hides the fact that we return a vector from the outside world, and instead just exposes the fact that "something that implements In the following code, the function body doesn't know anything about the type of fn v_consumer(v: impl Debug) { ... } This is exactly the same way in which a function using the return value of |
@torkleyy Thanks for giving this feedback about this feature - as the thumbs show, there are users who share your viewpoint. However, I want to deal with this RFC from a project-procedural perspective for the moment. The arguments you raise are not novel; in other words, they're all perspectives that we've considered before. Since the RFC has already been merged, and these arguments were made during the period before it was merged, I really don't see any reason we would revert that action now. However, merging an RFC doesn't mean the feature will become stable - all it means is that it will become available under a feature flag on nightly. Many features which were RFC merged have languished in instability or were changed significantly for one reason or another. It remains possible that your viewpoint will prevail. But we cannot constantly relegislate the same arguments again and again; if we did, whatever happens to be the status quo would always win, and the language will never make progress. Instead what really matters now is the new insights we gain from using the feature in real Rust code. What I want to encourage you to do, then, is wait until impl Trait in argument position can be used on nightly & then attempt to use it critically, and provide user feedback based on that experience. That feedback, from people with different perspectives, is what's most valuable when trying to make decisions in the phase we're in now. |
@withoutboats Thanks for the fast response.
I see.
Makes sense, I'll do that. Thank you again for explaining. |
Namely, it corresponds to the theorem of first-order logic (and of dependent type theory) that ∀ x: (P(x) → Q) if and only if (∃ x: P(x)) → Q. If you expand p → q as ¬p ∨ q, you can consider it a corollary of a form of de Morgan duality (∀ x: ¬P(x) iff ¬∃ x: P(x)) and distributivity of irrelevant quantifiers; if you set Q := ⊥ and ¬p := p → ⊥, you can consider it a generalised de Morgan duality. |
Fortunately, this duality between universal and existential quantifier holds even in fully constructive higher-order logics, no need to equate |
This RFC proposes several steps forward for
impl Trait
:Settling on a particular syntax design, resolving questions around the
some
/any
proposal and others.Resolving questions around which type and lifetime parameters are considered
in scope for an
impl Trait
.Adding
impl Trait
to argument position (where it is understood as anonymous, bounded generics)The first two proposals, in particular, put us into a position to stabilize the
current version of the feature in the near future.
Rendered
[edit: updated rendered link —mbrubeck]