-
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
Implied bounds #2089
Implied bounds #2089
Conversation
When I first learned Rust, having to repeat the constraints was a pain point that I didn't understand, and it wasn't really explained anywhere that I recall. I think the real way to teach this is to just modify all the guide's examples to take advantage, because then they do what you expect. Then maybe introduce something somewhere that explains that additional ones can sometimes be useful or spells out corner cases where you would need them that you wouldn't expect to. |
Nice job on the RFC! It's clear you've put a lot of careful thought into this feature (and its implementation). I'm really excited at the possibility of not having to repeat type bounds all over the place 😄. I'm interested in limiting the use of implied bounds to types defined in the current crate, as mentioned in the alternatives. I believe that this would address the areas where this feature is most-needed, while still giving library authors the flexibility to change their type bounds. (This flexibility made changes like RFC 1651 possible.) |
I've got one little question. Is this RFC limited to functions and impl blocks? Or does it also allow implying bounds for struct members? As in, is this code allowed with the RFC: struct Set<H :Hash> { ... }
struct Bar<H> { set_of_stuff :Set<H>, }
struct Baz<H> { bar :Bar<H>, } The summary only talks about functions and impl blocks, not about structs, so I guess its no, but maybe I've missed something. |
@est31 Would you find that desirable? I'd see a couple of problems there, especially that you could then build huge piles of types where it's hard to find out where the bound was propagated from. |
It does not allow implying bounds for struct members. It is plausible that we could enable that, but the current RFC draws the line around functions and impls. I wouldn't say it's a hard-and-fast rule, but as a rule-of-thumb, we've tended towards saying that struct definitions are more explicit than functions. This ensures that, when reading a function signature, you don't have to do a "deep search" to find out what is implied by (e.g.) This "rule of thumb" arose in part because, in the past, we didn't require explicit lifetime parameters on structs (they could always be elided, much as they are in functions). We found this quite confusing in practice: you would see That said, it has been pointed out (but I forgot by whom, maybe @RalfJung?) that sometimes when you make a small struct -- which is sort of a glorified tuple -- it would be convenient to infer bounds on its declaration. That seems true, but it's unclear where to draw the line precisely. |
Nope, quite the contrary in fact. That's why I was asking :). I've wondered whether this RFC would create the "deep search" problem that @nikomatsakis explained above. Great to have us all agree on this being a bad idea :). The RFC gets my 👍 now ;) |
So actually since the In general, I'm not a big fan of writing bounds on a type. But when you really need to (like the A problem which might arise though is when you want to use a type from another crate which does have bounds declared on it, as e.g. a private field of one of your own types. Then you're forced to have the same bounds on your type. I'm not against the idea of limiting implied bounds to types in your current crate if it is the general consensus though. |
So I am very much in favor of this general idea (and have been for some time now...since 2014, apparently...sheesh!). And I am very excited about the work that @scalexm has done to realize it in practice, formalize it, and work through some of the tricky implications. To my mind, the primary goal of this RFC is to eliminate the need to copy-and-paste redundant sets of bounds when implementing types (a random example). The RFC should also have the effect, however, of generally improving ergonomics, as also described in RFC #1927 and numerous Rust issues (e.g. rust-lang/rust#20671). Further, a nice side-effect of adopting the strategy laid out in this RFC will be fixing some of the various limitations in rustc's implementation (e.g., rust-lang/rust#20775). Overall, I think the "guts" of this RFC are a slam dunk! I'm inclined to move quickly to FCP. However, there were some interesting "judgement calls" raised in the discussion in the lang team, and I think it would be helpful to get some feedback on these points. I think there were two such concerns raised (both related):
To be honest, I think both of these things -- but especially the latter! -- are somewhat hard to judge in the abstract. They feel like concerns we might experiment with during the stabilization period. That said, we considered various ways to ameliorate these concerns, all basically focused on limiting the scope of the implied bounds:
Of the two, crate-local feels a bit better to me. We have plenty of precedent for crate-local rules of this kind, but none at all (very little?) for the other kind. Personally, though, I am inclined to start with the full version and see how it feels (e.g., within rustc and elsewhere). I think that if it feels surprising, we will start to notice it. That said, I think when we stabilize, if we still feel we want more experience, it would be reasonable to have a separate feature-flag for "cross-crate implied bounds". |
I agree the actual decision on "how far" we want implied bounds to go is probably best made after implementation or even during stabilization, but I wanted to suggest a middle ground between crate-local/module-local implications and "arbitrarily long distance" implications. Outside of the crate where a type Foo is defined, my current preference is for that type's bounds to be implied in any function that explicitly mentions Foo in its signature. For instance, I would want this to work:
But I would not want this to work:
For structs I'm a lot more wary of cross-crate bound implication, probably for the same reasons we don't do lifetime elision on structs today. I like this idea primarily because it means any implied bound that applies to your code must be coming from a function signature or type definition that you can see and look up, not an implementation detail you're not supposed to know about. But you still get the benefit of not needing the But that's all abstract theory we'll need experience to confirm so I'm 100% onboard with this RFC as-is. Just wanted to get that additional idea out there. Edit: And it turns out this is already exactly what the RFC says. I have no idea how I missed this the first time I read it, sorry! But it's a good sign that we ended up at the same place despite me apparently not paying attention. |
So actually this is already the case and your second example indeed does not work under this RFC (even when considering crate local-ness) :) I give an example in the guide-level explanation: // `Set<T>` does not appear in the fn signature: we need to explicitly write the bounds.
fn declare_a_set<T: Hash + Eq>() {
let set = Set::<T>::new();
} Same for struct fields, see e.g. @nikomatsakis comment. |
While learning Rust the circumstances when I could and could not elide bounds seemed very arbitrary to me. That made my first experience with Rust's generics a bit frustrating. I could not really reason about the rules and that made the learning considerably harder for me. Repeating the bounds every time (especially when writing data structures that forced various constrains on their types) cluttered up my code and made it hard to read. Imo, this change is really needed, streamlining the elision rules for bounds. It will make Rust's generics easier to learn and handle in practice. I'm all for starting with the long-range, full version and see how it goes during stabilisation. |
@jonastepe Could you give some examples? There's almost no places where bounds in Rust are elided, but for example this is legal: struct Foo<T> {
field: T
}
impl<T> Foo<T> {
fn print_out(x: T) where T: Debug {
println!("{:?}", x);
}
} It has nothing to do with elision though, just that bounds are taken into account at multiple places, so they don't need to be symmetric. |
@skade when writing |
@RalfJung Outside of lifetimes, are there any other examples, though? |
Well, one could consider supertraits something similar. Given But otherwise, nested lifetimes are the only one. However, fundamentally, the well-formedness condition regarding nested lifetimes is no different than the one regarding a type's trait bounds. |
@skade I'm referring specifically to lifetime bounds defined on a type. You could specify them on a type and decide not to include them (having them inferred) on an |
@rfcbot fcp merge This RFC has been open for 25 days and so far the comments seem uniformly positive. The one major question is precisely where bounds to permit implied bounds (as I outlined here. There hasn't been much commentary on that point, but honestly I think it's the sort of thing that would be best "discovered" by gaining some real-life experience from the stabilization process. Therefore, I propose that we merge this RFC, but add an unresolved question indicating whether we should try to limit the range of implied bounds to be crate-local (or module-local, etc). This just means we will revisit this question prior to stabilization and make sure we're happy with the result. |
Team member @nikomatsakis has proposed to merge this. The next step is review by the rest of the tagged teams: No concerns currently listed. Once these reviewers reach consensus, 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. |
Another possibility for the resolve-before-stabilization stuff: Do implied bounds allow use of those bounds in general, or only on the parameter that implied them (unless they're from Self, probably)? fn silly<T: Default>(x: std::collections::HashSet<T>) -> bool {
T::default() == T::default()
// ^ Can I still use == even though *I* never said PartialEq?
} |
@scottmcm We don't have a mechanism for restricting it, so it would be implied for all values of |
The final comment period is now complete. |
text/0000-implied-bounds.md
Outdated
|
||
I *think* rustc would have the right behavior currently: just dismiss this branch since it only leads to the tautological rule `(u8: Foo) if (u8: Foo)`. | ||
|
||
In Chalk we have a more sophisticated cycle detection strategy based on tabling, which basically enables us to correctly answer "multiple solutions", instead of "unique solution" if a simple *error-on-cycle* strategy were used. Would rustc need such a thing? |
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 think the case tabling comes into play is:
trait Bar {} // not implemented for anything
trait Foo {}
impl<T: Foo + #[cfg(maybe)] Bar> Foo for Vec<T> {}
#[cfg(maybe2)]
impl Foo for () {}
And then we ask whether ?0: Foo
. Currently, rustc will answer "ambiguous" for all 4 cases, while tabling might be able to figure out a more precise answer.
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.
Yes exactly, there is a test in Chalk for that precise example.
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 think that to be "concrete" the example also needs
struct u32 {}
impl Bar for u32 {}
To avoid the prover just seeing that Bar
has no possible impls.
IIRC one problem when we tried that was that we add bounds like |
text/0000-implied-bounds.md
Outdated
struct Set<K: Hash> { ... } | ||
struct NotHash; | ||
|
||
fn foo(arg: Set<NotHash>) { ... } |
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.
That's not correct in practice, because
- rustc will try to prove that
ENV(foo) |- WF(Set<NotHash>)
, because of one of the "the type of everything within a function must be well-formed" rules. - we theoretically have
WF(Set<NotHash>)
in our environment. Practically, we don't - the current implementation:
2.A. does not allow using environment predicates that aren't trait predicates, projection predicates, or "ParamWf" predicates. This is IIRC not done for a good reason, and is not needed.
2.B. ignores environment predicates that do not contain any parameters. This is done because these predicates either hold (and are therefore unneeded) or don't hold (and therefore can't be used in a valid program). This helps caching.
2.C. has "anti-getting-lost" rules. The rules "as written" mean, that if one has a traitThen there is a trait-system ruletrait Foo { type Bar: Sized; }
Then in order to prove that there is no impl forτ type WF(τ) τ: Foo ---------------- <τ as Foo>::Bar: Sized
[T]: Sized
, one has to check that we don't have a type[T] = <some-type as Foo>::Bar
. That can require going over all the impls to see that there is no applicable impl, so the current implementation in rustc does not use the trait-system rule unless it sees that the type is already a projection type.
This RFC has been merged! Tracking issue. |
I've been looking forward to this, but after a recent experiment where I attempted to make use of the existing feature of implied bounds on associated types, I am now concerned. Is it possible that the potentially large number of implied bounds introduced by this through traits may cause unexpected issues with trait selection in user code? See (The "recent experiment" I've alluded to is this code-genned trait (which I am having difficulty minimizing, so take it for what it is). I find that with the numerous |
@ExpHP Fixed rendered link, thanks. |
There should be a way to opt-out using something like struct Foo<T: Clone> {
v: T
}
impl<T> Foo<T> where T { // errors because T isn't Clone
} vs struct Foo<T: Clone> {
v: T
}
impl<T> Foo<T> { // builds
} vs struct Foo<T> {
v: T
}
impl<T> Foo<T> where T { // builds
} vs struct Foo<T: Clone> {
v: T
}
impl<T> Foo<T> where Foo<T> { // (aka `where Self`) builds
} (see comments and type bounds, should be self-explanatory.) |
@SoniEx2 It's not at all self-explanatory. |
@Centril I could be wrong, but I think that @SoniEx2 is suggesting that we only introduce implied well-formedness bounds in cases where the type is listed in the |
@cramertj I see, if that is the case then personally I agree that it is confusing both syntactically and in terms of behavior, and I also think that such knobs would lead to decision fatigue. |
Actually, explicitly listing a type parameter in Then you could also still have implied bounds with explicit bounds. e.g. struct Foo<T: Clone> {
v: T
}
impl<T: Eq> Foo<T> {
// benefits from implied bounds, T is implied as Clone + Eq
}
impl<T> Foo<T> where T: Clone {
// doesn't benefit from implied bounds, need explicit T: Clone (as shown)
} This makes the simple case simple, and the complex case possible. i.e. for some of the more complex type bounds where you'd need type parameters in a (Do you ever need where clauses for type parameters? Or do you only need them for like, |
as an alternative idea that doesn't require compiler figuring things out, maybe we could reuse
|
Eliminate the need for “redundant” bounds on functions and impls where those bounds can be inferred from the input types and other trait bounds. For example, in this simple program, the impl would no longer require a bound, because it can be inferred from the
Foo<T>
type:Hence, simply writing
impl<T> Foo<T> { ... }
would suffice. We currently support implied bounds for lifetime bounds, super traits and projections. This RFC proposes to extend this to all where clauses on traits and types.Rendered