-
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 for allowing eliding more type parameters. #1196
Conversation
since it becomes easier to type-hint things. E.g. the `random` | ||
example can becomes `random(..): u8`. | ||
- Require noting that parameters have been left to inference, | ||
e.g. `random::<u8, ...>`. This defeats much of the syntactic point, |
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 alternative is my personal preference (though perhaps with two periods rather than three).
I see why @huonw prefers his proposal, but on the flip side: How do I now specify "these are the exact type parameters, and if another gets added, I want to be told at compile time" ? (Think of it like not having a _ => ...
match arm...)
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.
(of course one might reasonably point out that we already do not have a way to express "these are the exact type parameters" when dealing with a struct/enum with default type params" ...)
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.
Do you have any cases in mind where it's important to know that one is specifying all of the parameters? It seems to me that most cases where adding new parameters requires external adjustment will fail to compile, but I could very very easily be wrong.
I assume you can only elide the last n type parameters this way? Would this interact with default type parameters? Like Felix, I think I would also prefer the dots - some evidence of the elision seems worthwhile. Generally we want this - we require it in patterns for example, so I think it is consistent. The only places we allow elision without some syntactic indication (I think) is where we elide everything - e.g., lifetime elision (one of one thing) or type inference (all type parameters). |
Yeah, only trailing parameters.
I assumed that omitting a default and writing |
What would happen if only one // before: collect::<Vec<_>>()
[1, 2, 3].iter()
.map(|&x| x)
.collect::<Vec<_>>();
// after: collect::<Vec>()
[1, 2, 3].iter()
.map(|&x| x)
.collect::<Vec>(); |
The proposed elision only applies to top-level parameters in expressions, not types, so no that doesn't work. |
+1 to this proposal, I've wanted this many times. |
Does it have any undesirable interactions with potential variadic generics? It is interesting, that full omission of generic parameters in brackets is currently allowed:
It looks like the alternative with |
I agree that it shouldn't be problematic with VG.
(FWIW, I don't think we should treat this as a bug, I like how Rust is quite flexible with "zero or more" and "allow trailing" rules, which help with macros and code-generation.) |
As another alternative, maybe use defaults to opt into this feature, e.g. |
Wait, in which of the above two categories are you placing default type parameters?
Attempted illustration of what I'm trying to ask about: http://is.gd/gT0ALq (At this point I am leaning towards a position that this RFC is consistent with our attitude towards default type parameters, for better or for worse. However, one might make the reasonable argument that up until now, elided things followed pretty simple rules to determine what was being elided -- I also wrote "syntactic rules" but I'm not 100% sure we can call the resolution of the type parameter defaults 100% syntactic...) |
@eddyb Just to clarify: Am I right that the semantics of your proposed alternative is: fn all_req<X, Y>(x: X, y: Y) { ... }
fn has_default<X, Y = i32>(x: X, y: Y) { ... }
fn can_omit<X, Y = _>(x: X, y: Y) { ... }
all_req::<char, _>(c, c);
all_req::<char>(c, i); // error: type parameter missing from instantiation
has_default::<char>(c, i); // okay
has_default::<char>(c, c); // error: `c` is not `i32`
can_omit::<char>(c, i); // okay
can_omit::<char>(c, c); // okay |
@pnkfelix That is exactly what I meant, yes. I originally thought of the lifetime counterpart like this, back when struct SomeContext<'tcx, 'a = '_> {
tcx: &'a TypeContext<'tcx>,
...
}
fn compute_type<'tcx>(cx: SomeContext<'tcx>) -> Ty<'tcx> {...} The Which reminds me, there's a subtle issue with type holes: they only exist at the AST level. fn infer_vec<X, Y = Vec<_>>(x: X, y: Y) { ... } The definition above would result in the internal type representation Although, even that is suboptimal: type DoubleMe<T> = (T, T);
fn double_trouble<A = DoubleMe<_>>(...) {...} The above use of a type alias should not expand to just any |
Concerning this example: fn random<T: Rand, D: IntoRand<T>>(d: D) -> T If we had the fn random<T: Rand>(d: impl IntoRand<T>) -> T This would achieve the same purpose. I suspect that a great many of the cases that cause type parameter omission to be desirable would also be satisfied by that proposal. Just something to consider. |
I do not believe we currently allow |
I agree with @nrc and @pnkfelix that using |
@nikomatsakis I was describing an issue that would arise if we just allowed My worries were unfounded, as type inference variables can still be used (with an extra instantiation step), but I wanted to document the case anyway. |
Yeah I've run into situations where it was necessary for the user to explicitly state one type parameter, but the rest were 'inconsequential' to the user, for example, a closure trait bound, so I did away with the idea entirely instead of requiring the user to do This would be nice. |
@blaenk I keep seeing these examples and wondering whether a trait method would've worked better. |
Yeah unfortunately I don't remember exactly what I was doing that I ran into that. But wouldn't that require me to do |
@blaenk No, I mean, splitting the type parameters into the type implementer aka |
This isn't obvious to me. That is, it's not obvious to me what the I'm not sure I understand the overall point of #1196 (comment). Am I correct that is basically "just" |
@huonw Well, |
In the language subteam meeting, we explored an interesting question about the interaction of this idea with defaulted type parameters. In particular, in types at least, if type parameters with defaults are omitted, this is equivalent to having typed the default value explicitly (versus creating a variable with fallback). For example, if I write this: struct Foo<T,U=T>(T,U);
...
let x: Foo<i32> = Foo(22, 'c'); I will get a compilation error, because This RFC is specific to fn/method references, so in a way there is no conflict here, but the discrepancy is somewhat unnerving. It feels like type parameters lists should behave the same with respect to defaults, regardless of what they are attached to. It's not entirely clear to me which behavior is better, for that matter, when it comes to types at least. The current behavior means that (e.g.) @huonw pointed out that the alternative of requiring an explicit fn foo<T>(..., x: u64) { ... } to fn foo<T,U=u64>(..., x: U) { ... } The default here helps to ensure that an integer literal like Thoughts? Hat tip @jroesch, who brought this interaction to my attention |
@nikomatsakis currently in the RFC it states that this sort of "mechanical fill in with Perhaps though the same confusion can arise just from using expressions? |
I just wanted to say 👎 This feels like bringing implicitness to a place which should be explicit. I like Rust because it forces you to be explicit about what you want in most cases. If you want type inference, you must explicitly request type inference. edit: nevermind. |
-1 to -1 to this RFC in general, at least for the moment. As mentioned in the proposal, we do expect general type ascription to land someday, which seems more composable than what this RFC describes while also not introducing more special cases between expression position and type position like this proposal does. At best, I'd say to postpone this RFC until after type ascription lands. |
I think the potential confusion and surprisingness of allowing it in other places is even worse: it feels natural for functions, but not so much for types, e.g. |
I think that's because you're eliding the brackets in addition to the types themselves. On the other hand, It could also be extended to allow |
What about |
This does make some amount of sense... |
I'm very confused. To me, the core motivation of this RFC is to make it so that you can add new type parameters to a function without breaking existing code. Of the various alternatives that have been floated, none seem to tackle this essential goal. Type ascription seems orthogonal. |
I think eddyb's did; it just adds the onus that when adding such parameters, one must declare them via either Update: of course further discussion up above points out places where the resulting semantics may be a bit weird when comparing behavior in type constructions versus expressions (as noted #1196 (comment) )
Hmm, okay that may be true; I may have not thought fully through what @bstrie was saying |
Maybe, but we don't write |
Ohh... this really wasn't obvious from the way the RFC was worded, at least to me. I'm still hesitant though. Isn't semver meant to solve these problems? Or is this a problem for the standard libraries, which have less flexibility to bump a major version for these kinds of changes? |
@huonw I am against a plain But that's not enough if the type was used outside of a function, in which case, there is no way to handle the backwards compat issue, except for the cases where is actually a sensible default to use (in which case, defaulting to inference is not what you get inside function bodies). |
31fbc39
to
b0267f8
Compare
I've now added a commit that adds API evolution as part of the motivation, since this RFC plays into that well, as @aturon has been pointing out. |
We discussed this a fair amount in the @rust-lang/lang meeting yesterday. The general consensus was that the proper interaction between this and fallback was unclear, but that was partly because the proper behavior of fallback itself is unclear. We didn't reach a firm decision, but as of the end of the meeting we were leaning towards:
Let me start with the reasons in favor of this RFC, and I'll discuss point 2 afterwards. I'm repeating points that have already been raised in the discussion thread here, but it's helpful to have it summarized. Basically there are two motivations:
Now, on the topic of the interactions with fallback for inference, there is growing concern from many of us that fallback is a bad idea. Experiments with implementing it revealed that there are a number of nontrivial decisions to make, and there are also strong composability concerns (basically, it's very easy to wind up with competing defaults, and there is no good way to resolve them). Defaults will also have trouble permitting forward evolution due to those same composability concerns as as well as the interaction with coercion: changing something from a concrete type to a default is potentially quite a big difference in terms of its interaction with other inference variables. Unfortunately, it seems that the compiler now accepts defaults on fns and, moreover, they act in a kind of inconsistent way. That is, if you write Some questions that arose in my mind as I typed this comment:
|
Thanks for taking the time to write that up, it's most appreciated! |
I as well didn't realize that the impetus was to allow API evolution without breaking changes, so I retract my objection. Still have a tickle in my mind like this is going to constrain us down the road, though. +1 to deprecating defaults on fns asap. |
How would you add a new type parameter to a function and guarantee that the calling code will break? Being able to do this is one of the key features of using a statically-typed language, especially one that is designed to use the static typing for safety-oriented programming. The proposal would be better if it were changed so that defaulting to the inferred type is only done when specifically requested, e.g.
|
Speaking as a Rust beginner: Restricting this to expressions only seems very arbitrary to me. The distinction is quite subtle. I think this would be quite difficult to explain in documentation. I know Rust is not a language designed for beginners but even relatively proficient programmers might not be aware of the distinction between statements and expressions. Unless they've had cause to work on a compiler there isn't really any good reason to know this anyway. As such these rules seem pretty arbitrary. Is there any good reason to allow this in one place and not in any other? I don't feel like the RFC lays out why this restriction is in place. Also it's unclear from the RFC if it should be legal to elide all type parameters in a case such as If it is restricted to eliding all type parameters except the first, then that's even more arbitrary, and even harder to explain. It feels to me like this RFC is kind of a half-way solution. Either go for full-on elision or don't do it at all. |
@jnicklas The difference is not between statements and expressions, but between paths with explicit type parameters, in both expressions and types. This is in part affected by/possible due the fact that inside functions there is type inference (and non-const expressions can only be found in functions). But it would be very surprising to me, I would like to see I guess I'm mostly worried about littering code with explicit types that give the wrong impression about their actual number of type parameters. |
For general consistency with the pattern matching syntax, I do like using |
I agree that this should be opt in, either using |
As a compromise, you could allow ellided type parameters in two ways - say
In the future the second version could be deprecated in favour of type ascription? This way existing code does not break and the ellision does not happen silently, too. Just my two cents. :-) |
The lang team discussed this RFC and related issues a final time yesterday, and determined that we'd like to close the RFC for the time being, and ultimately open a new RFC that covers not only the use cases here, but also the open questions about fallback in general. I'm planning to spearhead the effort of creating a comprehensive plan here. I'll be working with various stakeholders who would like to see improvements in this space for library ergonomics. @bluss, you're one of the main people I have in mind, but if others have specific use cases, please let me know! |
Rendered.