-
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: Associated type defaults #2532
Conversation
Awesome! I had kind of assumed anything fixing associated defaults would only have one-way-dependence relationship, but I'm pleasantly surprised to see this. The "if you override one, you override all" concept seems easy to understand, teach, and write. With that said, I am a bit sad to not have one-way dependence. If I have a bunch of default methods all relying on the same associated type default, I would want users to be able to override some default methods without overriding others. How open would you be to having Something with "more defaults" could be able to depend on something with "fewer defaults", but not the other way around. What I mean in codetrait Bar {
default {
type Foo = &'static str;
default const MY_FOO: Foo = "Bar's foo";
default fn get_fooa() -> Foo {
// can depend on Foo, but not on MY_FOO's value
"Foo A"
}
default fn get_foob() -> Foo {
"Foo B"
}
default {
fn get_c() -> Foo { "C" }
fn get_d() -> Foo { "D" }
}
}
}
struct Brick;
impl Bar for Brick {
// Can override get_foob without override get_fooa since they are both different sub groups
fn get_foob() -> Foo {
"Brick's Foo B"
}
}
struct House;
impl Bar for House {
// Since get_c and get_d are in the innermost group, must override them together
fn get_c() { "House C" }
fn get_d() { "House D" }
}
struct Cat {
type Foo = String;
// Cat must now override all other default methods, since they all are allowed to depend on Foo.
} This suggestion probably isn't ideal since it would mean writing |
What a great RFC, thanks! You already list many examples where the I just wanted to throw another idea out there: Trait items can never "depend" on a method, i.e. never depend on a specific method implementation, right? So in that case, we could just say "if you override one thing in a However, I'm not sure if this will be considered legal in the future: trait Foo {
const fn size() -> usize { 3 }
type ARR = [u8; Self::size()];
} If that's the case, other trait items could depend on specific implementations on methods and the modified rule wouldn't work. Also, I think that "letting the compiler infer all dependencies between trait items" (which you only mentioned briefly) is not that bad of an idea. Sure, the dangers of implicitness strike again, but I think it would have many benefits. Dependencies between trait items are a directed graph. The To solve the "implicitness-problem" one could:
|
Very much agree with this sentiment :)
I'm open :) It seems natural. In this scheme, the way we can understand The type checking rule, if I understand it correctly from your example, is that a sub-group acts as an atomic unit itself and can be overridden independently of its parent but if anything in the parent is overridden, then the sub-group must be as well. On the face of it, I think this scheme would be sound; but I would encourage everyone to double check this. This seems a bit more complex than "if you override one, you override all", but not by much. It also adds flexibility and value to nesting which was previously lacking.
Interesting :) If you have the time, could you perhaps encode this in the style of this RFC so that I can include it?
This seems sound; however, I see some drawbacks:
All in all, my view here is that needing to see the underlying type of an associated type won't be too common; in particular, I don't think it will be common to need to see the underlying type and have many methods at the same time. Given this conjecture, I think that the special casing of methods is not particularly justified here and that @daboross's amended mechanism of
So the reason I haven't gone into greater detail here is because the semantics of this inference have not yet been well defined and because it was only mentioned in passing in rust-lang/rust#29661 (comment).
Does not @daboross's amendment change this? It should be possible to define more refined sub-graphs this way?
This seems backwards compatible with this RFC in the sense that if you don't use
I think this is true; but I would like to see a more elaborate argument for why this is the case and how complex such inference would be.
First a few words about why implicitness is a problem in the first place. I think here, the problem is not so much that readability would suffer from implicitness, but rather that intuiting the dependencies for a human could be non-trivial wherefore it would be difficult to see what implications a change has for semantic versioning. The fear here is that the user would accidentally make a change that requires upstream users to provide definitions they previously didn't need to.
I assume this would also be checked by the compiler?
This doesn't seem to notably solve the semver problem; the crate author still has to scan the code to reconstruct the graph in their head.
I think this is highly likely to be the case. If it turns out to be a big problem (it is not in the Haskell community) then we can solve that problem in the future. |
Yes, now that I read their comment again, I think it's actually very powerful. Let's use this slightly modified version (I added trait Bar {
default {
type Foo = &'static str;
fn quux() -> Self::Foo { "quux" }
default const MY_FOO: Self::Foo = "Bar's foo";
default fn get_fooa() -> Self::Foo { "Foo A" }
default fn get_foob() -> Self::Foo { "Foo B" }
default {
fn get_c() -> Self::Foo { "C" }
fn get_d() -> Self::Foo { "D" }
}
}
} The above code would result in the following tree (each node representing one default group): As far as I understand (please correct me if I'm wrong): an item can depend on items in the same node and on items in any ancestor nodes (up the tree). This has the consequence that if an The "can depend on" rule sounds exactly like the rule we use to determine if a non- In terms of the dependency graph I was talking about, this would mean that the programmer can define ... "a tree of cliques" if that makes any sense. This looks really powerful to me. I'm not sure if there are actually useful situations where one would need a more powerful system. So I'm all 👍 for that idea!
True. That's always good.
I assumed it's possible, because that knowledge is already needed to emit compiler errors, right? To check if the trait is well-formed, the compiler has to check that there aren't any items depending on another item.
Yip, I agree with all of those. In particular, I also dislike special casing methods.
Mhhh, I'm not quite sure what you mean. But let me just explain some details.
My original motivation was to write something like this:
fn print<T>(x: T)
where
<T as RemoveRef>::WithoutRef: fmt::Display,
{
println!("{}", x.single_ref());
} Of course, for To have something like C++'s trait RemoveRef {
type WithoutRef;
fn single_ref(&self) -> &Self::WithoutRef;
}
default impl<T> RemoveRef for T {
type WithoutRef = T;
fn single_ref(&self) -> &Self::WithoutRef {
self
}
}
impl<'a, T: RemoveRef> RemoveRef for &'a T {
type WithoutRef = T::WithoutRef;
fn single_ref(&self) -> &Self::WithoutRef {
T::single_ref(*self)
}
} But this doesn't work since |
Yep; that seems right. (Also, nicely done on including a tree; I should integrate a similar thing in the RFC eventually).
This seems exactly right :)
That's perfect!
I think a "tree of cliques" makes perfect sense; and I agree that this should be more than enough power to do anything you need. I would like to triple-check the soundness implications of @daboross's amendment for a bit.
Hmm... It might be that to scan a trait definition to infer all dependencies, you must chase some indirections if items don't depend on each other directly. However, I haven't given it much thought.
Cool :) Aside: With #2289 you could write: fn print(x: impl RemoveRef<WithoutRef: fmt::Display>) {
println!("{}", x.single_ref());
}
So you would then write: trait RemoveRef {
type WithoutRef;
fn single_ref(&self) -> &Self::WithoutRef;
}
impl<T> RemoveRef for T {
default {
type WithoutRef = T;
fn single_ref(&self) -> &Self::WithoutRef { self }
}
}
impl<'a, T: RemoveRef> RemoveRef for &'a T {
default {
type WithoutRef = T::WithoutRef;
fn single_ref(&self) -> &Self::WithoutRef { T::single_ref(*self) }
}
} |
Off the top of my head, nested default groups seems like a good idea — however, I also think that we should ask whether functions deserve special treatment or not. From the point-of-view of "would things compile", I think that (at least with the lang as it is today) overriding a function can't cause any problems. But it might lead to semantic breakage. e.g., perhaps there is an associated type that is somehow "tied" to the details of what the fn does, and when those details change, a different type would be more appropriate, even though the older type would still compile. One can easily see this with some sort of associated constant. e.g., you might have a constant like Using nested groups, we can express the difference, which is good, but of course the notation sort of "defaults" the wrong way -- most of the time we don't need such strict dependencies, right? (I haven't had time to read the RFC in detail yet, I'm not sure if it discusses any such examples.) |
Me neither and I'm far to unfamiliar with the compiler internals to say how feasible this would be. Anyway, I guess the majority of the community would dislike this completely implicit version anyway.
I'm already subscribed to the tracking issue and hope that I can play with it on nightly soon ;-)
Exactly. That's what I meant. I think this should work.
But if you don't want those strict dependencies, you just don't use a And as I read the RFC, the meaning of |
I think that's exactly right; It doesn't even have to be an
I think most of the time, you won't need to assume the underlying definition of an item at all, and just the signature will be sufficient; i.e. I think most cases will be like the When the underlying type is needed to be assumed for some set of items, I think 1-deep will be the next most likely thing. Having two nest My conjecture about depth here is that the usage of depth decreases exponentially with the depth.
There is one example in the reference, but it is not a real world example.
would be the most common real world scenario for a 2-deep nesting.
Hehe, yes; I think so.
Feel free to implement it =P
Cool; I'll include it in the RFC as an example at some point ^.^
Yep; that's correct.
My understanding from #1210 was that |
Am I correct that everything here could be expressed with partial impls? And thus
|
No, you are incorrect. It is not sugar but adds a fundamental capability to the language that AFAIK can't be decomposed into anything else. |
Discussing briefly in the @rust-lang/lang meeting. Regarding my question here and the cycle here, all present agreed we can move these to an unresolved question. FWIW, given my current thinking about how Chalk should work -- and specifically lazy normalization -- actually i'm not sure where this error should be reported. :) I know i thought about exactly this case. I think the answer is most likely that it would fail at the impl, which would have the obligation of showing that the values for its associated types can be fully normalized -- but I can't picture the details just now. |
@rfcbot concern unresolved-questions Per my previous comment, I'd like to see these details added as unresolved questions so they are not forgotten. |
@rfcbot reviewed |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
Shall compiler error message mention that the thing after For example, something like this could be added:
Shall compiler error message when trying to use Currently: #![feature(associated_type_defaults)]
trait Lol {
type Q = std::fmt::Debug;
fn ror(&self, q : Self::Q) {
println!("{:?}",q);
}
}
Nothing here hints me that I am actually triggered the associated type default feature. (Obviously, assuming no P.S. Are compiler error messages and their novice-friendliness on-topic for RFC threads? |
We usually figure that out during implementation. :) |
The final comment period, with a disposition to merge, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank @scottmcm for their work and everyone else who contributed. The RFC will be merged soon. |
🎉 RFC 2532 is now merged as Tracking issue: rust-lang/rust#29661 (note, this is the old issue). |
Deny specializing items not in the parent impl Part of #29661 (rust-lang/rfcs#2532). At least sort of? This was discussed in #61812 (comment) and is needed for that PR to make progress (fixing an unsoundness). One annoyance with doing this is that it sometimes requires users to copy-paste a provided trait method into an impl just to mark it `default` (ie. there is no syntax to forward this impl method to the provided trait method). cc @Centril and @arielb1
Deny specializing items not in the parent impl Part of #29661 (rust-lang/rfcs#2532). At least sort of? This was discussed in #61812 (comment) and is needed for that PR to make progress (fixing an unsoundness). One annoyance with doing this is that it sometimes requires users to copy-paste a provided trait method into an impl just to mark it `default` (ie. there is no syntax to forward this impl method to the provided trait method). cc @Centril and @arielb1
🖼️ Rendered
⏭ Tracking issue
📝 Summary
Resolve the design of associated type defaults, first introduced in RFC 192, such that provided methods and other items may not assume type defaults. This applies equally to
default
with respect to specialization. Finally,dyn Trait
will assume provided defaults and allow those to be elided.💖 Thanks
To @aturon for their work on RFC 192 and RFC 1210 upon which this RFC builds.
To @kennytm, @Havvy, @ubsan, @varkor, @alexreg, and @scottmcm for reviewing the draft version of this RFC.