trait_alias_impl
Extend #![feature(trait_alias)]
to permit impl
blocks for trait aliases with a single primary trait. Also support fully-qualified method call syntax with such aliases.
Often, one desires to have a "weak" version of a trait, as well as a "strong" one providing additional guarantees. Subtrait relationships are commonly used for this, but they sometimes fall short—expecially when the "strong" version is expected to see more use, or was stabilized first.
Send
bound aliasesImagine a library, frob-lib
, that provides a trait with an async method. (Think tower::Service
.)
Most of frob-lib
's users will need Frobber::frob
's return type to be Send
, so the library wants to make this common case as painless as possible. But non-Send
usage should be supported as well.
frob-lib
, following the recommended practice, decides to design its API in the following way:
These two traits are, in a sense, one trait with two forms: the "weak" LocalFrobber
, and "strong" Frobber
that offers an additional Send
guarantee.
Because Frobber
(with Send
bound) is the common case, frob-lib
's documentation and examples put it front and center. So naturally, Joe User tries to implement Frobber
for his own type.
But one cargo check
later, Joe is greeted with:
Joe is confused. "What's a LocalFrobber
? Isn't that only for non-Send
use cases? Why do I need to care about all that?" But he eventually figures it out:
This is distinctly worse. Joe now has to reference both Frobber
and LocalFrobber
in his code, and (assuming that the final AFIT feature ends up requiring it) also has to write #[refine]
.
#![feature(trait_alias)]
What if frob-lib
looked like this instead?
With today's trait_alias
, it wouldn't make much difference for Joe. He would just get a slightly different error message:
Iterator
This example relies on some language features that are currently pure speculation. Implementable trait aliases are potentially necessary to support this use-case, but not sufficent.
Ever since the GAT MVP was stabilized, there has been discussion about how to add LendingIterator
to the standard library, without breaking existing uses of Iterator
. The relationship between LendingIterator
and Iterator
is "weak"/"strong"—an Iterator
is a LendingIterator
with some extra guarantees about the Item
associated type.
Now, let's imagine that Rust had some form of "variance bounds", that allowed restricting the way in which a type's GAT can depend on said GAT's generic parameters. One could then define Iterator
in terms of LendingIterator
, like so:
But, as with the previous example, we are foiled by the fact that trait aliases aren't impl
ementable, so this change would break every impl Iterator
block in existence.
Async
traitThere has been some discussion about a variant of the Future
trait with an unsafe
poll method, to support structured concurrency (here for example). If such a change ever happens, then the same "weak"/"strong" relationship will arise: the safe-to-poll Future
trait would be a "strong" version of the unsafe-to-poll Async
. As the linked design notes explain, there are major problems with expressing that relationship in today's Rust.
With #![feature(trait_alias)]
(RFC #1733), one can define trait aliases, for use in bounds, trait objects, and impl Trait
. This feature additionaly allows writing impl
blocks for a subset of trait aliases.
Let's rewrite our AFIT example from before, in terms of this feature. Here's what it looks like now:
Joe's original code Just Works.
The rule of thumb is: if you can copy everything between the =
and ;
of a trait alias, paste it between the for
and {
of a trait impl
block, and the result is sytactically valid—then the trait alias is most likely implementable.
A trait alias has the following syntax (using the Rust Reference's notation):
Visibility?
trait
IDENTIFIER GenericParams?=
TypeParamBounds? WhereClause?;
For example, trait Foo<T> = PartialEq<T> + Send where Self: Sync;
is a valid trait alias.
Implementable trait aliases must follow a more restrictive form:
Visibility?
trait
IDENTIFIER GenericParams?=
TypePath WhereClause?;
For example, trait Foo<T> = PartialEq<T> where Self: Sync;
is a valid implementable alias. The =
must be followed by a single trait (or implementable trait alias), and then some number of where clauses. The trait's generic parameter list may contain associated type constraints (for example trait IntIterator = Iterator<Item = u32>
).
There is another restriction that trait aliases must adhere to in order to be implementable: all generic parameters of the alias itself must be used as generic parameters of the alias's primary trait.
impl
blocksAn impl block for a trait alias looks just like an impl block for the underlying trait. The alias's where clauses are treated as if they had been written out in the impl
header.
Bounds on generic parameters are also enforced at the impl
site.
If the trait alias uniquely constrains a portion of the impl
block, that part can be omitted.
Alias impl
s also allow omitting implied #[refine]
s:
Trait aliases are unsafe
to implement iff the underlying trait is marked unsafe
.
Implementable trait aliases can also be used with trait-qualified and fully-qualified method call syntax, as well as in paths more generally. When used this way, they are treated equivalently to the underlying primary trait, with the additional restriction that all where
clauses and type parameter/associated type bounds must be satisfied.
Implementable trait aliases can also be used with associated type bounds; the associated type must belong to the alias's primary trait.
trait Foo = Bar + Send;
means something different than trait Foo = Bar where Self: Send;
will likely be surprising to many.
impl
blocks, which Rust programmers already understand.+
bounds of implementable aliases.trait Foo = Bar + Send;
could be made implementable).
+ Send
and where Self: Send
would no longer be present.impl
blocks would be broken.#[implementable] trait Foo = ...
. This would make the otherwise-subtle implementability rules more explicit, at the cost of cluttering user code and the attribute namespace.It's possible to imagine an extension of this proposal, that allows trait aliases to be implementable even if they have multiple primary traits. For example:
Such a feature could be useful when a trait has multiple items and you want to split it in two.
However, there are some issues. Most glaring is the risk of name collisions:
Such a feature could also make it harder to find the declaration of a trait item from its implementation, especially if IDE "go to definition" is not available. One would need to first find the trait alias definition, and then look through every primary trait to find the item. (However, given the current situation with postfix method call syntax, maybe this is an acceptable tradeoff.)
Perhaps a more narrowly tailored version of this extension, in which both subtrait and supertrait explicitly opt-in to support sharing an impl
block with one another, would satisfy the backward-compatibility use-case while avoiding the above issues. I think exploring that is best left to a future RFC.
rustdoc
render these? Consider the Frobber
example—ideally, Frobber
should be emphasized compared to LocalFrobber
, but it's not clear how that would work.where
clauses more powerful would make this feature more powerful as well.
Future
→ Async
use-case.trait Foo: Copy = Iterator;
could be allowed as an alternative to trait Foo = Iterator where Self: Copy;
.impl
bodies could be expanded, for example to support combining supertrait and subtrait implementations.Attendance: TC, tmandry, scottmcm, eholk, waffle, Jules Bertholet, pnkfelix, nikomatsakis, jubilee
Minutes, driver: TC
wffl: I don't think that even with implementable trait alias you can do this[1], because most iterator adaptors don't work on lending iterator iirc.
tmandry: More explicitly that means that those iterator adaptors wouldn't be implemented in the lending case? That would be a poor user experience but could still be done right?
jubilee: to what end tho'? Iterator exists for its user experience, essentially. If we wanted a unifying abstraction that was actually good, we could find a way to let people write two blanket impls so that the UnifiedIterator could actually define a unified interface in a useful way, but if I recall correctly we can't because we can't do
even if we know Iterator and LendingIterator are never implemented for a given type.
TODO(wffl): write an example
nikomatsakis (not stated in meeting): I generally agree with what I think jubilee is saying, which is that lending iterators and iterators are fundamentally different in terms of the ownership relations, and while we should absolutely say that every iteratr can be used where a lending iterator is expected, they are not generally unifiable in the same way that Fn
and FnMut
and FnOnce
are not unifiable (i.e., when I take a impl LendingIterator
, I am agreeing to use that value in a more limited range of ways – the values I get will not beyond a single next
call – in exchange for giving more power to the implementor to reuse data and storage).
jubilee: right, it's closer to the reverse, of "LendingIterator is strong, Iterator is weak, because the constraints on the LendingIterator's use is actually stronger, much like the requirement for a Send bound is actually a stronger constraint".
TC: The document mentions overlap with trait transformers. NM, could you discuss whether and how you view any overlap here?
NM: It did surprise me to see that in there. I suppose there is some overlap. One of the use-cases is adding Send bounds. That particular one could be handled by trait aliases. But others could not. Such as traits with methods that are maybe async.
NM: This makes sense as a more limited extension.
wffl: it's a bit weird that the first restriction in reference explanation is syntactic, rather than semantic. this probably does not matter, but it's weird
scottmcm: I find the phrasing in the reference section of the rules for what's implementable using a grammar as strange. = Foo + Bar;
vs = Foo where Self : Bar;
being this different doesn't seem like it ought to be meaningful. (Especially if it leads to people needing to write trait aliases as =where Self: Foo, Self : Bar
to not be implementable, or something.)
(copied up higher to go with waffle's same point.)
wffl: what the title said, it's not immediately clear why you need to use generic parameters specifically as generic parameters of the trait. For example I could imagine impl Foo<T> for Type<T> {}
being valid if Foo<T>
doesn't use T
in the trait.
wffl: similarly this looks fine:
Jules Bertholet: The idea is to avoid surprising overlap errors. For example, in the example just above, adding a Foo<i32>
impl would give an overlap error, even though syntactically there seems to be no conflict.
Jules Bertholet: Also, if Foo<T>
doesn't use T
, how should impl<T> Foo<T> for ()
be handled? I wanted to avoid all those questions by just saying "no".
NM: It may be that none of these rules are adequate. We're saying this desugars to a series of impls for the underlying traits. E.g.:
NM: So, rather than imposing this restriction at the trait site, we could impose it at the impl site?
waffle: Yes.
NM: This is a good question, but maybe a minor point in some sense. We can accept more things by making the check at the impl site. But there is some fundamental limit to how accepting we can be.
waffle: In some sense it's more consistent to do it at the impl site. If you're returning something like impl Foo<u32>
…
TC: Would it be backward compable to loosen this?
wffl: I believe so.
tmandry: We could add this as a future possibility or open question.
TC: I know people want to discuss extending this to multiple traits or otherwise relaxing the rules on whether a trait alias is implementable. What do people think about this?
NM: I want us to support auto traits.
pnkfelix: Not good enough to support it in Where Clauses?
NM: Not sure what we gain by that?
pnkfelix: Cut and paste semantics.
NM: But it's not cut-and-paste semantics. E.g.:
Jules: That's correct, that's one exception to the cut-and-paste.
NM: If we look at the motivating example of Send bound aliases. The definition is pretty complex. Adding a bound to return type… that's going to be checked at the impl site I assume. It doesn't seem like cut-and-paste semantics.
waffle: Allowing auto traits is more confusing than less confusing. I'd expect that if you have:
waffle: If we allow auto traits, then we're implementing the trait but not requiring it.
NM: You're effectively saying that those WCs that I'd expect to be enforced at the impl site are not part of the impl, they're not implied by the impl existing. They may be part of WF checks.
tmandry: Currently they'd be where clauses on the impl. So you could even implement this alias for a type that never implements Send
…
NM: So you'd get an error when you try to use the impl. Today you might even get an error on the impl itself, but we intend to relax that.
scottmcm: The big reason for allowing multiple here is that it would be really valuable to allow someone to split a trait in two. And you can't do that if you can't implement both of those parts through the same impl. That's a reasonable wish for people to have of this feature.
waffle: I think that it's reasonable to support implementing multiple traits, but only if all traits joined by +
are implemented. So this would support splitting a trait, but not having +Send
.
scottmcm: I'd agree with that.
NM: This is annoying possible truth. I don't want people to have to write where Self: Send
.
NM: The set of supertraits are the set of both things.
NM: I also feel that auto traits are maybe just different.
scottmcm: #[marker]
traits are again something that you have to implement, so You'd just implement it.
NM: It's kind of an assertion. People write dummy functions for this, just to check Send.
pnkfelix: NM made the correct point it's not a cut-and-paste semantic. Reading the reference, the way this is supposed to work is…
It's as if you had…
Any occurrence of MyTrait<u32>
you now get to infer that item. The combination of the trait definition and the impl block gets you that. Strange but true.
NM: It is strange, in some sense.
scottmcm: It makes me think we should allow writing it that way.
NM:
jubilee: it has some confusing consequences for the fact that different traits use associated types like "type Output" for wildly different reasons.
NM: I like this RFC more if we're more ambitious, agreeing with scottmcm.
pnkfelix: There are interesting form of reasoning this gives you access to.
NM: That's really a result of trait aliases rather than this RFC.
tmandry: pnkfelix, are you reacting to this in impl position or in WC position?
pnkfelix: In a WC position.
pnkfelix: …I withdraw any negative feedback associated with my surprise.
waffle: Agree it would be cool to support just writing the thing down in normal traits. We should reach out to T-types to make sure they don't hate us for this.
NM: We should talk with this RFC in general with T-types.
NM: But to the degree that this is a desugaring, it doesn't really land in T-types territory. But there may be some caveats.
NM: We should strive to it desugaring to a bunch of impls that you could have rewritten in another way. And to the degree we can't do that, maybe we should make our impl
syntax more flexible.
scottmcm: This is making me think of things where the alias wants its own associated type, like imagine:
scottmcm: Can I write one like above where I didn't fully qualify that?
NM: pnkfelix, example where I think this confusion arises today
NM: For better or worse, that's possible today.
NM: One of the things I want is that in subtraits, adding bounds to associated types. Maybe that works here?
scottmcm: hmm, on nightly where T: Iterator<Output: Ord>
is valid syntax, right? I guess in a trait alias that one desugars to a where
in the way that <Output = Foo>
doesn't?
NM: …
TC: Even ahead of this meeting, people were excited about this one. Is there anything more we need before proposing FCP merge?
pnkfelix: Can we move forward with what this is saying and later do something more ambitious?
NM: I don't think so?
scottmcm: It gets back to what we think the purpose of an RFC is. Do I want an experiment on this tomorrow? Yes. But is this the RFC I want, not sure.
waffle: It'd be good to discuss the future possibilities we discussed in the RFC.
tmandry: We do change details of RFCs in the stabilizaton process.
Jules: I'll add more things to the future possibilities, especially around the rules on trait parameters.
Jules: The discussion has raised questions for me around where clauses and whether what has been proposed is the best way. I'm whether whether the copy-and-paste model of the impl doesn't really apply.
scottmcm: My meta-take-away from this meeting is worry about trait aliasse trying to do two different things. That might be some of the confusion around WCs.
(The main body of the meeting ended here.)
TC: Perhaps, as NM proposed, we could lean more heavily on the desugarings, and expand the RFC to include those desugarings, and then separate out any things that can't be expressed as a desugaring.
waffle: Maybe we could write a separate RFC for:
waffle: If this could be accepted separately, this RFC gets simpler.
Jules:
waffle:
wffl: A problem with leading iter and normal one:
…and ArrayChunks
does not make sense for a lending iterator, because you can't have two items from a lending iterator at the same time.
…so you'd have to write something like…
…and this breaks impls of Iterator
(which can technically overwrite array_chunks
):
…but maybe that's not true because the Self: Iterator
bound would be trivial and you don't have to write it in the implementation?..
(The meeting ended here.)
pnkfelix: Proposal as written makes implementability a sem-ver guarantee, right? (I.e., someone who writes a trait alias may not realize that what they have written is implicitly implementable, and then it becomes a sem-ver breaking change to add a + Copy
or similar … but then again, is any such change already sem-ver breaking? Not sure…)
pnkfelix: I was thinking about this as an argument in favor of the #[implementable]
attribute attached to such aliases, but its a weak argument at best.
TC: It's another good reason to consider how loose we could plausibly make the rules for implementability.
tmandry: The example using blanket impls with RTN is how someone might approach this once we have RTN. Is it possible to a library to migrate from this…
…to a trait alias, without breaking backwards compatibility?
For that matter, we don't actually have RTN today, so someone might write this instead…
…which I think could be migrated to the blanket impl with RTN (see here for rationale), and hopefully also to trait aliases.
nikomatsakis: In essence, I think a trait alias…
…should be shorthand for this…
With, I think, no real differences at all between them except perhaps this one, where you can implement an Alias
directly…
…though that would be effectively "expanded" to impls of the bounds (eventually I'd like you to be able to implement a trait and its supertraits in one impl, but that's separable).
What's missing here. Maybe there is no question and this is just me thinking out loud. I guess for thing the where Self: Send
rule doesn't seem needed.
scottmcm: It talks about allowing impl Foo<T = i32> for Bar
via a trait alias. Can it be done directly?
This was also Jubilee's immediate reaction. "Isn't this a lifetime relationship that most Iterator stuff would 'violate' in some way?" ↩︎