rfcs
rfcs copied to clipboard
RFC: Implementable trait aliases
Allow writing impl blocks for most trait aliases. Also, allow trait aliases to have bodies.
@rustbot label A-traits
As an aside, it'd be nice if type aliases permitted instantiation:
pub struct FooT<T>([T; 32]);
pub type Bar = Foo<u8>;
impl Default for Bar {
fn default() -> Bar {
Bar( Default::default() ) // Forbidden right now.
}
}
As an aside, it'd be nice if type aliases permitted instantiation:
they do, if you use curly braces (except for tuples): https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b95fe39281037ac567cfc8dfa7aba44b
i think it'd also be nice to allow curly brace syntax for tuples for ease of writing proc macros so there's one consistent syntax that works on all struct-like types:
type T<A, B> = (A, B);
let v = T::<_, _> { 0: "abc", 1: 123 };
match v {
T::<_, _> { 0: a, 1: b } => println!("it works! a={a} b={b}"),
}
they do, if you use curly braces (except for tuples): https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b95fe39281037ac567cfc8dfa7aba44b
Wait, so T { 0: 5 } works but T(5) doesn't? This sounds like a bug.
Wait, so
T { 0: 5 }works butT(5)doesn't? This sounds like a bug.
Changing this behavior would be a breaking change, for code like the following:
struct Foo();
type Bar = Foo;
fn Bar() {}
Anyway, this is all a bit off-topic for the RFC :grin:
I think that the RFC should mention #1672 and mutually exclusive traits
https://github.com/rust-lang/rust/issues/20400 https://geo-ant.github.io/blog/2021/mutually-exclusive-traits-rust/ https://stackoverflow.com/questions/57749827/mutually-exclusive-traits
Is this RFC an alternative to #1672 or is it orthogonal?
@nielsle I don't see any relationship between this RFC and that issue.
I think the summary motivation section should be fleshed out more, ideally with real-world examples (possibly simplified). After looking through the text this does seem useful but it wasn't obvious at all to me what the feature was from those sections.
I've specified that implementable trait aliases also support fully-qualified method call syntax.
@rustbot +I-lang-nominates +I-types-nominated
I'm nominating this for @rust-lang/lang and @rust-lang/types discussion. I myself am in favor of this RFC. I've seen a lot of demand for a simplified version of trait aliases where--
(A) We only support trait Foo = Bar + Baz format, essentially.
(B) The trait Foo is implementable and, in general, acts like a "normal trait" so that users don't have to know about the more detailed version.
I expect this to be relevant to async fn in traits as well (cc @tmandry) it's quite common to have something like trait Service (which requires Send) and trait LocalService (which does not).
Restricting to point (A) has the advantage of avoiding some complex corner cases. Note though that I do want to support the "inline bound" syntax, so that you could do trait SendIterator = Iterator<Item: Send> (as well as RTN if we adopt that), so that trait aliases can put bounds on associated types.
I guess that my question to the lang/types teams, respectively, are:
- Lang: what do people think about the motivation here as well as the more limited proposal? Are there important trait alias use cases not covered there?
- Types: are there concerns about the limited proposal that I put forward?
One though -- @Jules-Bertholet -- I'm not sure if the RFC covers it, I didn't have time to read in super detail, but I think that if you are implementing trait Foo = Bar + Baz, that implies "unioning" the methods from Bar and Baz into one impl block. Does the RFC discuss this?
I have, for a long time, wanted the ability to implement a trait and its supertraits together in a single impl block, I wonder if it's worth thinking about that as well, though I'd probably want to separate it out from this RFC.
One though -- @Jules-Bertholet -- I'm not sure if the RFC covers it, I didn't have time to read in super detail, but I think that if you are implementing
trait Foo = Bar + Baz, that implies "unioning" the methods fromBarandBazinto one impl block. Does the RFC discuss this?
The RFC does not do any sort of trait unioning like this, I hadn't even considered it as a possibility (I will add it to the alternatives section). Notably, you would need to handle name collisions.
I've added text to the alternatives section addressing @nikomatsakis's "unioning" idea.
@Jules-Bertholet
Notably, you would need to handle name collisions.
Yes. Presumably that would be an error.
Presumably that would be an error.
That would have backward-compatibility implications, if one of the traits adds a defaulted method that conflicts. Technically "minor" breakage, but still not ideal.
@Jules-Bertholet good callout -- I think the error would have to be if your impl contains a single function item that is ambiguous.
First impression: I'd love to have a way to split an existing trait into two (to pull out a more limited version) that wasn't a breaking change. So big 👍 from me on doing something that can help that kind of thing.
I'm also a fan of making trait aliases feel more like traits, since right now I tend to think of them as "bounds aliases" since you can't use them in a bunch of places where you can use a "normal" trait. (Unlike type aliases, which can be used in most (all?) places where normal types can be used.)
I have a couple of worries after skimming through this, though.
My biggest worry is in interactions with the accepted https://rust-lang.github.io/rfcs/2845-supertrait-item-shadowing.html. That made adding a method to a trait less of a breaking change again, but I think that this would effectively undo it -- anyone implementing through a trait alias like this would be broken in those cases, I think. The "obvious" fix there would be to have people write which trait the item they're implementing came from, but that violates my wish from the first paragraph in this comment about being able to split traits, so I don't like it at all. As such I'm not sure how best to address this. Maybe some kind of coherence restriction on what traits can be combined could work? Not sure.
I'm also worried about the "can you paste it into an impl block" rule of thumb. As the RFC mentions about syntactic distance, having "the same" trait alias behave differently depending whether it's written with + or where feels quite surprising to me. TBH, I feel like if I wrote trait Qux = Foo + Bar; and I'd heard that trait aliases were implementable, I'd expect to be able to impl Qux for MyType { ... } and have it just work to write the methods in that block. Having it unclear whether the author intended for it to be implementable feels much like dyn to me, where I often wish there was a way to be more intentionable about what you expect consumers to do with the trait.
Spitballing:
- Should maybe
trait Foo =always be implementable? And use a different construct for bounds aliases that aren't implementable in the same way traits are? - What constraints exist to allowing something like
impl Foo+Bar for MyTypeorimpl Ord for MyType { fn cmp(...; fn partial_cmp(...; }? Could we find rules that allow this for more cases? Is there some horrible downside to it we could put in a design note about why we'll never do that?
My biggest worry is in interactions with the accepted https://rust-lang.github.io/rfcs/2845-supertrait-item-shadowing.html. That made adding a method to a trait less of a breaking change again, but I think that this would effectively undo it -- anyone implementing through a trait alias like this would be broken in those cases, I think.
Is this concern meant to apply only to the extended proposal that allows multiple "primary" traits? The RFC in its current state would not have this issue.
We discussed this some in our @rust-lang/lang meeting today. We didn't come to a full consensus but we raised a few points:
- People in meeting seemed generally positive about being able to implement a subtrait and supertrait at the same time.
- The where the where GAT discussion, in my personal view, effectively deprecated the
where Foo = where Barsyntax, so I don't think we should lean on that. - We were also interested in saying "you can't implement every trait alias, but you can do it a lot of the time".
To the last point, I was floating specifically the idea that we can use the same rules that we use to define when one method takes precedence over another to decide what method is being implemented.
For example, if you had
trait Foo { fn method1() { ... }}
trat Bar: Foo { fn method2() { ... }}
impl Bar {
fn method1() { ... } // from Foo
fn method2() { ... } // from Bar
}
and then Foo adds a fn method2() { ... }, we could say that the impl is still legal but the Foo::method2 is defaulted.
(Of course, if Bar added a fn method1() { ... }, that would be a breaking change, but this is true anyway, because code that invokes x.method1() would now be calling Bar::method1 and not Foo::method1. Fragile base classes are hard.)
In general, my preference would be to say that trait Foo: Bar + Baz and trait Foo = Bar + Baz are basically the same and usable in all the same places in all the same ways.
People in meeting seemed generally positive about being able to implement a subtrait and supertrait at the same time.
This may or may not be a good idea, but in either case I think it is out of scope for this RFC.
The where the where GAT discussion, in my personal view, effectively deprecated the
where Foo = where Barsyntax
What syntax, exactly? I've improved the language around how where clauses are treated by this RFC, hopefully that addresses your point?
I've added another restriction on which trait aliases are implementable, to avoid problems around overlapping impls. Generic parameters of implementable type aliases now need to be used as generic parameters to the alias's primary trait.
Another random thought: it would be nice if trait SelfAdd = Add<Output = Self>; was implementable as well. That would give a path to moving a type to an associated one without breaking things, and just be convenient for cases where there's a particularly-common choice for one or more associated types.
Another random thought: it would be nice if
trait SelfAdd = Add<Output = Self>;was implementable as well.
It is intended to be. The RFC has an example like this already, but I will improve the language to make it more clear that this is allowed.
What syntax, exactly? I've improved the language around how
whereclauses are treated by this RFC, hopefully that addresses your point?
I think Niko was saying that we shouldn't have clauses like where Self: Send in trait aliases but should instead make trait aliases like trait Foo = Bar + Send implementable.
@rustbot labels -I-lang-nominated
This was discussed in the T-lang design meeting today. People do seem to want this in some form, but the RFC is likely going to need some iteration. Let's remove the nomination while we give time for this iteration to be done. Please renominate when you think T-lang should look back into this.
I've made some changes in response to the design meeting (Zulip thread, HackMD) held earlier today. Notably, I've:
- Clarified the language around how and where
wherebounds are enforced.- The new rules bring this RFC more or less in line with @nikomatsakis's model of "desugaring to a blanket impl."
- Removed the restriction that implementable trait aliases must use all generic parameters as parameters of their primary trait.
- Added a few more sentences on why the syntax is the way it is.
One thing I haven't yet updated is the motivation section. Many people have pointed out that they would like motivating examples that don't rely on unstable features. I plan to get to that Soon™—but in the meantime, if anyone has a good real-life example, feel free to share.
Also in the design meeting, many people had thoughts on this RFCs relationship to LendingIterator/GATification. Because that part of the RFC is so speculative, I don't think it would be wise to expand on it here, but perhaps we should discuss this in another forum (Zulip or Internals?).
I found the motivation in the RFC a bit off-target. I think that the 'weak-strong' concept is correct, but not really specific to trait aliases. Every subtrait corresponds to a "refined" view of the supertrait (sometimes with extra capabilities/methods as a result of that refinement). I think of trait aliases as really just a shorthand for declaring refined traits in cases where no "per-type behavior" is needed (i.e., because there are no new methods, or even (eventually) because the definition of the methods is always the same).
To my mind, the real motivation here is expanding on the idea of trait aliases as being able to provide a "complete abstraction", where users don't have to know about the "contents" of the alias, they can treat it "as if" it were a standalone trait. I think the RFC started in the right vein there -- trait aliases are used to provide a "simplified, convenient view" onto a more complex underlying reality, but they're not able to truly hide that from users.
Eventually I would like it if users could do the same for any trait -- i.e., if I have a trait Foo: Bar, I should be able to implement Foo and Bar together etc. But I don't think that has to be in this RFC, and it'll take a bit of thought to figure out how it should work best. Still, the intution makes sense to me -- i.e., if I am in some OOP language, I don't have to separately define Shape and Rectangle and Square, I can just define Square and that Rectangle/Shape come "for free" with that.
I think of trait aliases as really just a shorthand for declaring refined traits in cases where no "per-type behavior" is needed (i.e., because there are no new methods, or even (eventually) because the definition of the methods is always the same.
I agree with this framing, that's a good way of putting it.
I've added a subsection to the future possibilities that explores allowing trait aliases to define their own associated types and consts, and the additional backward-compatibility benefits that would bring.
Having given it some more thought, I now suspect that associated types in trait aliases could be sufficient to GATify Iterator without a separate "variance bounds" feature:
pub trait LendingIterator {
type LentItem<'a>
where
Self: 'a;
fn next<'a>(&'a mut self) -> Option<Self::LentItem<'a>>;
}
pub trait Iterator = LendingIterator
where
// Still need to resolve implied `'static` bound problem
// (https://blog.rust-lang.org/2022/10/28/gats-stabilization.html#implied-static-requirement-from-higher-ranked-trait-bounds)
for<'a> Self::LentItem<'a> = Self::Item,
{
type Item;
}
associated types in trait aliases could be sufficient to GATify Iterator without a separate "variance bounds" feature
I've added a section to the future possibilities discussing this.