Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upRFC: Associated type defaults #2532
Conversation
Centril
added some commits
Aug 23, 2018
This comment has been minimized.
This comment has been minimized.
ubsan
commented on text/0000-assoc-default-groups.md in a9ab28b
Aug 27, 2018
|
Gotta love mcbride :) |
Centril
added some commits
Aug 27, 2018
Centril
added
the
T-lang
label
Aug 27, 2018
Centril
self-assigned this
Aug 27, 2018
This was referenced Aug 27, 2018
This comment has been minimized.
This comment has been minimized.
daboross
commented
Aug 28, 2018
•
|
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 |
This comment has been minimized.
This comment has been minimized.
|
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:
|
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
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
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 |
This comment has been minimized.
This comment has been minimized.
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) }
}
} |
This comment has been minimized.
This comment has been minimized.
|
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.) |
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
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 |
Centril
referenced this pull request
Sep 2, 2018
Open
When using associated_type_defaults: Why do I need to reimplement method with default impl when overriding assoc type default? #53907
Centril
referenced this pull request
Sep 12, 2018
Merged
Named existentials and impl Trait variable declarations #2071
This comment has been minimized.
This comment has been minimized.
burdges
commented
Sep 17, 2018
|
Am I correct that everything here could be expressed with partial impls? And thus
|
Centril
removed
the
I-nominated
label
Jan 31, 2019
This comment has been minimized.
This comment has been minimized.
|
I think the latest updates resolved the controversial points, so let's get more eyes on it: @rfcbot fcp merge |
This comment has been minimized.
This comment has been minimized.
rfcbot
commented
Jan 31, 2019
•
|
Team member @scottmcm has proposed to merge this. The next step is review by the rest of the tagged team members: Concerns:
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. |
rfcbot
added
proposed-final-comment-period
disposition-merge
labels
Jan 31, 2019
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
@rfcbot concern unresolved-questions Per my previous comment, I'd like to see these details added as unresolved questions so they are not forgotten. |
This comment has been minimized.
This comment has been minimized.
pnkfelix
reviewed
Feb 15, 2019
text/0000-assoc-default-groups.md Outdated
This comment has been minimized.
This comment has been minimized.
|
@rfcbot reviewed |
rfcbot
added
the
final-comment-period
label
Feb 15, 2019
This comment has been minimized.
This comment has been minimized.
rfcbot
commented
Feb 15, 2019
|
|
rfcbot
removed
the
proposed-final-comment-period
label
Feb 15, 2019
gnzlbg
reviewed
Feb 20, 2019
text/0000-assoc-default-groups.md Outdated
This comment has been minimized.
This comment has been minimized.
vi
commented
Feb 20, 2019
•
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? |
This comment has been minimized.
This comment has been minimized.
We usually figure that out during implementation. :) |
rfcbot
added
finished-final-comment-period
and removed
final-comment-period
labels
Feb 25, 2019
This comment has been minimized.
This comment has been minimized.
rfcbot
commented
Feb 25, 2019
|
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. |
Centril
merged commit 0c80f82
into
rust-lang:master
Feb 25, 2019
This comment has been minimized.
This comment has been minimized.
|
Tracking issue: rust-lang/rust#29661 (note, this is the old issue). |

Centril commentedAug 27, 2018
•
edited
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
defaultwith respect to specialization. Finally,dyn Traitwill assume provided defaults and allow those to be elided.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.