This is a tracking issue for trait aliases (rust-lang/rfcs#1733).
TODO:
unexpected definition: TraitAlias
INCOHERENT_AUTO_TRAIT_OBJECTS
future-compatibility warning (superseded by https://github.com/rust-lang/rust/pull/56481)Unresolved questions:
?Sized
bounds in trait objects, ban them "deeply" (through trait alias expansion), or permit them everywhere with no effect?I think #24010 (allowing aliases to set associated types) should be mentioned here.
I'd like to take a crack at this (starting with parsing).
I read the RFC and I saw a call out to Service
, but I am not sure if the RFC actually solves the Service
problem.
Specifically, the "alias" needs to provide some additional associated types:
See this snippet: https://gist.github.com/carllerche/76605b9f7c724a61a11224a36d29e023
Basically, you rarely want to just alias HttpService
to Service<Request = http::Request>
You really want to do something like this (while making up syntax):
trait HttpService = Service<http::Request<Self::RequestBody>> {
type RequestBody;
}
In other words, the trait alias introduces a new associated type.
The reason why you can't do: trait HttpService<B> = Service<http::Request<B>>
is that then you end up getting into the "the type parameter B
is not constrained by the impl trait, self type, or predicates" problem.
Basically, you rarely want to just alias HttpService to Service
Rarely? How do you define that?
The syntax you suggest seems a bit complex to me and non-intuitive. I don’t get why we couldn’t make an exception in the way the “problem” shows up. Cannot we just hack around that rule you expressed? It’s not a “real trait”, it should be possible… right?
@phaazon rarely with regards to the service trait. This was not a general statement for when you would want trait aliasing.
Also, the syntax was not meant to be a real proposal. It was only to illustrate what I was talking about.
I see. Cannot we just use free variables for that? Like, Service<Request = http::Request>
implies the free variable used in http::request<_>
?
@phaazon I don't understand this proposal.
@durka: how's the work on the follow-up to https://github.com/rust-lang/rust/pull/45047 going?
Something I mentioned in the RFC: trait Trait =;
is accepted by the proposed grammar and I think that this is a bit weird. Perhaps maybe the proposed _
syntax might be more apt here, because I think that allowing empty trait requirements is useful.
We can put a check for that in AST validation. However I suppose it could
be useful for code generation if there's no special case, I dunno.
On Tue, Feb 27, 2018 at 12:48 PM, Clar RoĘ’e notifications@github.com
wrote:
Something I mentioned in the RFC: trait Trait =; is accepted by the
proposed grammar and I think that this is a bit weird. Perhaps maybe the
proposed _ syntax might be more apt here, because I think that allowing
empty trait requirements is useful.—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/rust-lang/rust/issues/41517#issuecomment-368965137,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAC3n3HdG3ZOmbN2PKky7nFnx0WokY7Lks5tZD_fgaJpZM4NGzYc
.
One other thing to note as a general weirdness is macro expansions involving trait bounds. Currently, fn thing<T:>()
is valid syntax but perhaps fn thing<T: _>()
should be the recommended version.
But then, is _ + Copy
or something okay? I'm not sure. I would just suggest Any
but that has different guarantees.
Empty bound lists (and other lists) are accepted in other contexts as well, e.g. fn f<T: /*nothing*/>() { ... }
, so trait Trait = /*nothing*/;
being accepted is more of a rule than an exception.
I think it makes sense being accepted, although I wonder if making it the canonical way to do so outside of macros is the right way to go. We already have been pushing toward '_
for elided lifetimes in generics, for example.
So I was thinking of working on this one because it's a feature I really want and it seems relatively straightforward, but I'm not exactly sure where to start. I haven't done any compiler work before.
It seems like a good first implementation would be to basically convert trait aliases to a trait and blanket impl in the HIR, but I'm not sure if that would actually work.
@clarcharr That sounds like a good plan to me, on first sight, though I'm not the most knowledgeable about this. I wonder if someone could help you by mentoring. Maybe get yourself on the Discord #lang channel and ask? :-) (Also, I might even even contribute here myself, given this seems to be a fair bit of work.)
One thing I don't see mentioned in the RFC is whether use
ing trait aliases brings methods into scope? (see the next comment for a correct example, mine was wrong).
@Nemo157 In your example you are using a trait object, which should work (as well as generics) without importing the trait, like this:
mod some_module {
pub trait Foo {
fn foo(&self);
}
}
fn use_foo(object: &some_module::Foo) {
object.foo();
}
The question is whether this should work:
mod some_module {
pub trait Foo {
fn foo(&self);
}
pub trait Bar {
fn bar(&self);
}
pub struct Qux;
impl Foo for Qux {
fn foo(&self) {}
}
impl Bar for Qux {
fn bar(&self) {}
}
pub trait Baz = Foo + Bar;
}
use some_module::{Baz, Qux};
fn use_baz(object: Qux) {
// Should this work because Baz is in scope?
object.foo();
object.bar();
}
Considering I want this more than even async/await, I'll try diving in here. Are there any possible mentoring or hints as to where to proceed? It looks like @durka got parsing in place, and in typeck collect
, it specifically says it's not implemented. I'll likely start there, trying to copy what a regular trait does, and then watch as ICEs pile up...
It would be useful to update the issue description with more details about the current status. It took me a good half hour to figure this much out:
libsyntax
knowing how to parse it.If specific issues could also be listed, it'd help keep track of the work remaining.
@seanmonstar Awesome! Your impression seems right. I couldn't get it integrated in the trait resolver at all. Here is some old advice from @nikomatsakis. Suggestions I got didn't work, and it seemed like chalk was going to come and change the whole system -- but I have no idea how that's progressing.
@seanmonstar Sounds good! I too have heard that Chalk may be the way forward with implementing something like this, but I’m not completely sure. Anyway, depending on how much work ypu feel this may be, I’d be glad to chip in if you like.
I want to clarify something that I don't think was properly addressed in the original RFC discussions. From what I gather, the feature that has been accepted is not "trait aliases", but "bounds aliases". However, the proposals to use keywords other that trait
for this seem to have been completely ignored.
From experience, using incorrect terminology for things causes problems down the line that we might not guess at now, because it introduces inconsistencies (especially with a keyword that is already used for something else: specifically just traits).
Personally, I'd really appreciate a convincing argument for why trait
is an acceptable keyword for bound aliases, or a proposal to change the syntax to something like bounds
(or constraints
, or really anything more general) before stabilisation.
@varkor Fair point. I agree bounds
would be a more accurate name, and lead to less confusion.
FWIW, I don't think using trait
is actually that confusing. Type aliases do not define new types themselves, and yet we use type Foo = ..
syntax. Trait aliases may have edge cases where they don't include any traits (?Sized + 'static
or something), but that doesn't mean they won't appear to function as traits or even necessarily need to use a different keyword.
As example of a related place where people use slightly incorrect terminology to no ill effect, we often hear the bounds on a trait's Self
parameter referred to using the language of inheritance- "base trait," "supertrait," etc. Despite the fact that there's no subclassing going on, and currently not even a way to downcast trait objects, this is still fine.
but that doesn't mean they won't appear to function as traits or even necessarily need to use a different keyword.
I think trait
is very misleading, precisely because this point is false. If "trait aliases" worked like type aliases, then you would be able to do something like the following:
trait Trait {}
trait Alias = Trait;
impl Alias for () {}
As it stands, you cannot, because so-called "trait" aliases are actually bounds aliases. Traits and bounds are different (even specifically _trait_ bounds), both from an abstract model point-of-view, and their functionality within the language.
Using the term "trait alias" for this terminology is confusing and prevents true trait aliases from being added in a consistent manner with type aliases.
As example of a related place where people use slightly incorrect terminology to no ill effect
The use of terminology by people is very different to its realisation in the language. People might say "trait" instead of "trait bound" in practice, but conflating the two inside the language is, I would argue, a mistake.
I believe that in practice there shouldn't be a visible difference between trait Alias: Trait where Self: Other {} impl<T: Trait> Alias for T where Self: Other {}
and trait Alias = Trait
.
So, in this case, impl Alias
doesn't even make sense, because you've already defined a blanket impl.
@clarcharr: I'm thinking more in terms of renaming, like in the following:
struct S;
trait T1 {}
trait T2 = T1;
impl T2 for S {}
This is the intuitive meaning (as far as I can see it) of the term "trait alias". It's analogous to how type aliases work. Note that you would _also_ be able to use these trait aliases in bounds (aliases would be functionally identical to traits).
Whereas "bounds aliases" would alias both traits and lifetimes, specifically in the context of bounds on generic parameters, etc.).
I assume you mean impl T1 for dyn T2
, right? Or impl<T: T2> T1 for T {}
?
The latter will work as expected. Although the former basically, what you're asking is the meaning of what impl T2
and dyn T2
mean. Which… again, I think work in the context of trait aliases as both trait aliases and bound aliases.
@clarcharr: sorry, I got that line completely muddled. I've updated it. I mean that we implement a trait by using its alias. (Obviously in this example, there's no point, but you could imagine something with generic parameters, etc. being useful to use in this way.)
anecdata, but I would find bounds
to be more confusing than trait
.
We actually did go back and forth on whether to use the keyword bound
or maybe constraint
in the RFC, but didn't come to a firm conclusion. Personally I don't see the huge need for a new keyword. You can use a trait as a bound... you can use a trait alias as a bound.
Would it make it better if you could impl
an alias? We talked about that too but there are some gotchas. I very much encourage those now debating "trait"/"bound" to read these comments as some issues were already brought up.
The important thing is to get it implemented so we can experiment with these things.
You can use a trait as a bound... you can use a trait alias as a bound.
Yes, it works in one direction but not the other. You can use a trait as a bound. You cannot use a bound as a trait. If we _did_ want to introduce actual trait aliases in the future, we've blocked the obvious keyword.
We talked about that too but there are some gotchas.
Those aren't gotchas. Those are precisely the differences between traits and generic bounds. The issue is exactly that those two things are being conflated, just because a generic bound can consist of a single trait.
We can't just pretend that traits and bounds are the same thing. They have practical difference, which as @nrc pointed out in the original thread, are certainly going to cause people unfamiliar with the feature confusion.
Both features seem useful. If, in theory, we wanted both features in the language, we couldn't use the same keyword, because it's ambiguous in the case of a single trait. Therefore we _need_ to make this distinction. Using trait
, so that you can do something like trait Alias = Foo + Bar;
is absolutely going to confuse beginners about what a trait actually is, because it seems to be saying that Foo + Bar
is a trait.
Thanks for finding the discussion @durka, I certainly remember having this conversation so I know it wasn't ignored.
In general, on a lot of the tracking issues for unimplemented extensions, I've seen people revisit questions from the RFC, usually syntactic ones. Syntactic questions are some of the hardest & most subjective to make a call on, and in my experience, when there's no actual implementation, the "hypothetical" questions get exhausted after a short time, and the conversation kind of goes in circles after that. I think one of the points of the RFC FCP period is to put a stop on hypothetical conversations until we actually have an implementation which can inform our decision again.
That is, I would personally find it more productive if we waited on re-opening syntactic or "bikesheddy" conversations on the tracking issue until we have an implementation to give more feedback into the conversation.
Using trait, so that you can do something like trait Alias = Foo + Bar; is absolutely going to confuse beginners about what a trait actually is, because it seems to be saying that Foo + Bar is a trait.
Yes, Foo + Bar
is not a trait and I truly think that the terminology used in Rust is wrong. A bound – the terminology – shouldn’t even exist. The correct terminology to talk about here is a constraint.
trait Foo { /* … */ }
Foo
is a trait which implementation yields a constraint. People can use that constraint with the Foo
identifier.
trait Bar = Quux + Zoo + 'static;
Bar
is a trait which implementation yields a constraint. People can use that constraint with the Bar
identifier.
Using the current terminology, trait Trait { … }
defines the constraint / contract a type must implement whenever it appears in a bound and trait Trait = Something;
doesn’t define anything but expose a new compound contract a type must implement whenever it appears in a bound. A trait is never actually used as-is in the current Rust language definition as you always need to use a trait bound. And a bound doesn’t make sense if it’s not bound to a free type variable. When you define you trait alias, you have no such free variable, so there’s no bound here.
So, nah, I don’t think people will get confused if the feature is explained in sufficient clarity. Maybe the terminology of Rust should be enhance to clearly make the difference between:
@withoutboats: I have no objection to features being implemented to get a feel for them. What my concern is that after a feature is implemented, it's stabilised while glossing over the potential problems that were not properly addressed during the RFC process. I don't care if my point is addressed now, but I would appreciate it being considered before stabilisation. The tracking issue is the location for design comments now, and as you say, since the design is not stable yet, that means it's open for discussion. I'm leaving my comments here so they can be taken into account at the right time, or discussed.
@phaazon:
A trait is never actually used as-is in the current Rust language definition as you always need to use a trait bound.
It may appear this way on the surface, which is why I think this is a subtle issue. A trait does exist as a standalone concept in the Rust language (just like lifetimes do), specifically in the context of traits applied to types. <Type as Trait>
for instance, is about an "instance" (forgive the OO terminology) of a trait. Any time you directly call a trait method on a value, you're making use of the fact that traits are a concrete concept in the language. But in order to express that something _even can be treated in this way_ requires a corresponding trait bound. Trait bounds are exactly what allow you to use traits.
A bound – the terminology – shouldn’t even exist. The correct terminology to talk about here is a constraint.
Personally, I don't really mind what terminology is used here (although "bound" is consistent with existing usage). I just want to emphasise that there is (both a theoretical and a functional) difference between: traits; lifetimes; generic bounds / trait bounds / constraints.
Maybe the terminology of Rust should be enhance to clearly make the difference
I'm always up for clarifications of terminology, especially in situations like this where the distinction is subtle. My point really boils down to wanting to clarify the terminology in-language as well as in documentation.
Sorry, I know my comments might be coming off a little bit antagonistic, and that's not what I'm intending!
(Again, this post need not be addressed now, or for a long time. I want to post it now because I actually had time to read through the entire RFC thread, so it's fresh in my mind. My thoughts are not going to change after an implementation — the feature's intention is already clear. I would have raised the same point if I'd been around during the RFC period.)
Essentially, I feel like the decisions that were made in the original RFC mirror those that were made in favour of argument-position impl Trait
, which (regardless of my or your particular opinion) has proven to be an unpopular choice (compared to the majority of Rust features which are accepted with strong support).
I want to make a few comments in response to https://github.com/rust-lang/rfcs/pull/1733#issuecomment-285912355 (I know it was made a long time ago, but I think it's still relevant).
I don't think its a good idea to "up front" the understanding of the distinction between traits and bounds. Users can get pretty far not knowing the difference, its once they are more expert these sort of distinctions can be uncovered.
This is very like the argument that "users can get pretty far with an intuitive understanding of impl Trait
, in which case argument-position and return-position just make sense". The problem is that:
(a) either users will muddle along, not noticing the oddity ("oh, I must just not understand Rust enough yet")
(b) they will be confused by what the difference is, trying to work out the distinction (not realising that the syntax is actively misleading them, making these concepts even harder to learn to become a better Rust programmer)
(c) eventually, when they learn the difference, they will realise there's an inconsistency (they may or may not care at this point, as these discussions have shown)
This comment asserts that (a) is most likely to happen and when (c), most users won't care. But it's not fair to make that assertion, as someone who's been involved in the design process and understands Rust much, much better than a beginner.
In addition there are the negative externalities of introducing more keywords and especially more contextual keywords.
Agreed. The disadvantage with acknowledging them as bounds aliases is that you need a new keyword. But there's a disadvantage in using an existing keyword in that you either:
(a) can't implement _actual_ trait bounds in the future, because the keyword is already used
(b) have to overload the meaning (which is inconsistent, ugly, and leads to confusing concepts for users — see above point)
I'm sure many of those involved with the discussions have programmed in languages filled with inconsistencies and warts. From a "language beauty" perspective, this is really ugly. It's sacrificing temporary convenience for practicality, with a very minor gain. I'm not sure how, understanding what traits, lifetimes and bounds actually mean in Rust, one could look at the following and not feel any smidgen of regret:
trait Foo = 'static;
This approach does not help. We need to make things simple for the users. And, as I've been repeatedly reminded myself, assuming we know what's easier for beginners is not something we (as more experienced users of the language) can make. My feeling is that using the trait
keyword for bounds is going to be more confusing to users than a new keyword.
I think trait is very misleading, precisely because this point is false. If "trait aliases" worked like type aliases, then you would be able to do something like the following:
I was agreeing with @varkor precisely for this point. Type aliases are not really a misnomer as "trait aliases" would be, since you can still use them in virtually all the places type names can be used, as far as I remember.
i.e.
impl Trait for TypeAlias { ... }
works, but
impl TraitAlias for Type { ... }
does not work according to the above RFC. Nor is their an intuitive way to make it work.
Calling them bounds aliases is just much more accurate.
(The trait Foo = 'static;
example is also a good argument against calling them "trait aliases", though I think the above is the strongest point.)
@alexreg Your second code block should probably be
impl TraitAlias for Type { ... }
I've also tried the first thing and it does work. I wonder why anybody would want to do that but oh well.
@varkor Your point about <Type as Trait>
actually seems like a much stronger argument for renaming this feature to me: If there were actual trait aliases (as in aliases for a single trait that could be used as in the example above), it would make a lot of sense to also support <Type as TraitAlias>
. I'm not so sure if it would make sense with this feature – it might be handy in a very small amount of cases, but wouldn't at all make sense for things like trait Foo = 'static;
.
Re. "bound aliases": Is this not the difference between a bound and a constraint?
impl<T> Send for Arc<T> where
T: Send + Sync + ?Sized,
~~~~~~~~~~~~~~~~~~~~ constraint
~~~~~~~~~~~~~~~~~~~~~~~ bound
And in that case, wouldn't it a lot more sense to talk about "constraint aliases"?
@alexreg Your second code block should probably be
Yep, copy & paste coding fail, thanks for pointing it out! I fixed it now.
As for <Type as TraitAlias>
, I considered this too, but since this makes no sense where TraitAlias
can actually be a bounds alias including multiple traits, it reinforces the case.
It seems like everyone seems to agree about the fact that the term _bound alias_ is more accurate than _trait alias_. Would this syntax suit us?
pub bound StaticDebugClone = 'static + Debug + Clone; // (1)
pub constraint StaticDebugClone = 'static + Debug + Clone; (2)
It would then preclude the problem about implementing a _trait alias_ – since implementing a bound doesn’t make sense.
Also, we should cope with HRTB. Is this possible then?
trait RefTrait<'a> {
type Target: 'a;
fn ref(&'a self) -> Self::Target;
}
bound Free = for<'a> RefTrait<'a>;
fn test_it<T>(t: T) where T: Free {
let target = t.ref();
// …
}
I’m especially interested in how we should cope with factored existential quantification, i.e.:
where for<'a> T: Foo<'a> + Bar<'a>
If we use the constraint terminology instead, we can for instance do this:
constraint FooBar<'a> = Foo<'a> + Bar<'a>;
where T: for<'a> FooBar<'a>
Re. "bound aliases": Is this not the difference between a bound and a constraint?
impl<T> Send for Arc<T> where T: Send + Sync + ?Sized, ~~~~~~~~~~~~~~~~~~~~ constraint ~~~~~~~~~~~~~~~~~~~~~~~ bound
Correct, good point! Internally in the compiler, things like Trait+Send
or for<'a> Trait<'a>
-- things you can put after a T:
in a where clause -- are called "bounds". T: $bound
is one example of a "where predicate", and in its most general form it has another level of quantification: for<...> T: $bound
. There are two other kinds of where predicates: region predicates ('a : 'b
), and equality predicates (which are unstable). "constraint" is not used in actual type names / enum variants, from what I can tell, but it is used in comments and seems to be the same as a "where predicate".
So "bound alias" would probably be the correct terminology, but TBH I find that it sounds rather awful.
factored existential quantification
This is universal quantification. ;)
But yes, for<'a> T: Foo<'a> + Bar<'a>
is a "where predicate". T: for<'a> Foo<'a> + Bar<'a>
is an equivalent predicate. You transformed your predicate/constrained from the first to the second form when introducing the alias, but that should not change its meaning.
If we use the constraint terminology instead, we can for instance do this:
These are not constraints/where predicates though -- Foo<'a> + Bar<'a>
is a bound.
@RalfJung
"constraint" is not used in actual type names / enum variants, from what I can tell, but it is used in comments and seems to be the same as a "where predicate".
The terminology comes from Haskell where Eq :: * -> Constraint
and Int :: *
and Eq Int :: Constraint
. Here, *
is the kind of types and Constraint
is the kind of "satisfying a type-class".
Traits are therefore type-level functions from types to a "fact of implementation".
I think folks should take a look at Data.Constraint. It might be a useful experience to understand that stuff.
Now that we have an implementation in nightly, it appears my earlier question about whether a trait alias brings methods into scope is no. Using @pengowen123's test case on the playground gives
error[E0599]: no method named `foo` found for type `some_module::Qux` in the current scope
Is this the intended design, or just a current implementation limitation?
@Nemo157 Good question. I think it should work, but it wasn't really a concern of my initial PR. I should be submitting another PR today, but after that focus can turn to various things. Do you fancy having a go at this issue? I could possibly advise.
Would be curious what @nikomatsakis thinks of this too.
@withoutboats There are outstanding issues with trait aliases, but I think the first box can be checked in the original PR comment. The initial PR was merged some time ago, after all. :-)
@Nemo157 Actually, would you mind opening a separate issue about that, which references this tracking issue?
@alexreg opened #56485
Not sure if it is a known bug or not, but if you have a pub trait Asdf = Clone;
or whatever, cargo fmt
strips off the pub
which can then cause errors about exposing a private Trait.
@ricochet1k Best to file as an issue on the rustfmt repo. Thanks! https://github.com/rust-lang/rustfmt/issues
@ricochet1k @alexreg Thanks.
On a side note, currently putting auto
in front of a trait alias is allowed:
#![feature(trait_alias)]
auto trait Foo = Iterator<Item = u8>;
fn main() {}
Is this intentional? If not, could we change this to a syntax error? FWIW putting unsafe
in front of a trait alias is a syntax error.
@topecongiro Definitely an oversight. Thanks for the report.
Hit another ICE today reported in https://github.com/rust-lang/rust/issues/59029
Since https://github.com/rust-lang/rust/pull/59166 landed, is it possible to mark the second item #56485 as complete and begin the documentation/stabilization process?
@davidbarsky I don't think it's going to get stabilised quite yet... there are some outstanding questions about whether we want to generalise this system (mainly by other individuals).
@alexreg Understood—when you say “we want to generalize this system” are you referring to the above discussion on higher-rank type bounds or something else?
(Apologies if I'm having you restate things that were clearly state earlier—I'm just entering this discussion for the first time and I didn't see anything in the above conversation about generalization beyond the HRTB-related discussion.)
@davidbarsky: I'm not sure whereabouts this is recorded, but there has been some concern about whether "bounds aliases" (i.e. the current behaviour for "trait aliases") or "constraint aliases" (where you explicitly parameterise over the type being bound) are the most useful notion of alias. For example, with trait aliases, you cannot encode type equality constraints (see https://github.com/rust-lang/rust/issues/20041, for example).
As a separate, but related issue, there are learnability concerns about the naming convention. Namely, there are three related, but distinct concepts that could be considered "trait aliases":
These questions at least will need to be resolved before stabilisation.
A 4th question is how this all integrates with "trait generics" and "associated traits" as well. My goal is to ensure that we end up with flexible & a coherent system for all of this. However, it is difficult to test this out without having associated traits & trait generics on nightly. Thus, I think stabilization of this should wait on that.
It'd be helpful to add steps of what's blocking this to the top comment, and if possible, links to something describing what they are. For instance, this is the first I've read about "associated traits". Are "trait generics" meant to describe generics on associated types (GATs)?
@seanmonstar e.g. fn foo<trait T>(...)
, foo::<Ord + Eq>
and trait A { trait B; }
. See https://github.com/Centril/rfc-trait-parametric-polymorphism/ for more (very WIP).
@Centril woah! Very interesting idea...
Looking through the original trait aliases RFC, I see ContraintKinds
listed as an alternative. Since it's listed, I assume it was discussed, and it didn't hold up the RFC merging. Has something come up since that reverts that decision?
@seanmonstar I just read through all of the comments in the RFC; ConstraintKinds
as in having trait X
universal quantification and as an associated item was barely discussed. bound
vs. trait
was discussed partially; I'm not sure I agree entirely with the rationale. constraint Foo<T>
was discussed but I think we need to actually test the limits of the system with associated traits and trait generics to gain confidence in the solution. IOW: we need more data with a fully implemented and coherent system.
ConstraintKinds as in having trait X universal quantification and as an associated item was barely discussed.
Ah, that's too bad. I'm a fan of keeping tracking issues to status updates, so would it make sense to move discussion of whether something like ConstraintKinds
should block the progress here to something else? Like an IRLO thread or something?
@seanmonstar I don't think I have the time for such a discussion but go ahead if you want. My view is that there would need to be really good arguments why we should stabilize this before associated traits & such are even in nightly. Given that trait aliases are mostly a matter of ergonomics it all seems like quite a risky endeavor to me. As such I'm at least for now not inclined to put trait aliases on a clear path to stabilization.
@Centril Trait parameters and associated traits sound potentially interesting but this is the first I hear of them. Are they on any roadmap? Is there consensus in the lang team that they should block existing features (with an accepted RFC and an implementation) like trait aliases, or is that your personal opinion?
Are they on any roadmap?
Nope.
is that your personal opinion?
Yep.
I think the stabilisation process includes consideration of interaction with possible future features, if insufficiently evaluated during the RFC process (and, indeed, I think some of these concerns were not realised until after the RFC was accepted). Besides that, though, there are issues with trait aliases that have been raised in the existing discussion (and which turn out to be relevant to @Centril's concerns).
Is it possible to stabilize subset of trait aliases without complicated things, but still usable for things like trait IncomingFunction = Fn(Many,Parameters) -> RetType;
before resolving tricky concerns?
@vi: the existing concerns are about the entire feature, rather than particular details (e.g. the current syntax may not be appropriate).
@Centril FYI added an unresolved question for the matter of maybe bounds in trait objects.
Hi! TBH I don't know if this topic was covered or not, but let me express here one serious concern. The current state of the world in Rust regarding code duplication is bad. As someone coming from the Haskell world, I'm often terrified how much code I need to copy-paste here and there and there is no way to generalize it (unless I put every file in one big macro, which honestly, is not a solution). One of the most important missing elements for me is the ability to abstract trait bounds (read that as "create a type-alias for several trait bounds"). You know, if we've got in Haskell a code like
foo :: (T t, S s, P p) => t -> s -> p
I can create type MyCtx t s p = (T t, S s, P p)
and re-write that code to:
foo :: MyCtx t s p => t -> s -> p
Right now, it is impossible in Rust. However, this proposal could change that and could allow us for that. In fact, the syntax is already there, but ... it doesn't really pick the constraints from the alias. Here is an example code, which creates an alias for multiple trait bounds, but does not compile (while I think it should):
#![feature(trait_alias)]
/////////////////////////////
/// HasFoo Generalization ///
/////////////////////////////
// Just a trait
pub trait HasFoo {
fn foo(&self);
}
// Type Family
pub trait HasChild {
type Child;
}
// Nice accessor for Type Family
type Child<T> = <T as HasChild>::Child;
/////////////////////
/// Example Usage ///
/////////////////////
pub struct Test<Child> {
data: Child
}
impl<Child> HasChild for Test<Child> {
type Child = Child;
}
// Creating a type alias for multiple bounds
trait TestCtx = where
Self: HasChild,
Child<Self>: HasFoo;
// THIS ONE COMPILES
impl<Child: HasFoo> Test<Child> {
fn test(&self) {
self.data.foo()
}
}
// THIS ONE DOES NOT COMPILE
impl<Child> Test<Child> where Self: TestCtx {
fn test2(&self) {
self.data.foo()
}
}
Error:
#1 with rustc nightly
error[E0599]: no method named `foo` found for type `Child` in the current scope
--> <source>:48:19
|
48 | self.data.foo()
| ^^^ method not found in `Child`
|
= help: items from traits can only be used if the type parameter is bounded by the trait
help: the following trait defines an item `foo`, perhaps you need to restrict type parameter `Child` with it:
|
46 | impl<Child: HasFoo> Test<Child> where Self: TestCtx {
| ^^^^^^^^^^^^^
error: aborting due to previous error
If this code will be accepted, we will have a very powerful missing piece of the type system in Rust.
EDIT
For the purpose of completeness, here is an analogous code in Haskell (if anyone would be interested):
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
class HasFoo t where
foo :: t -> ()
data Test child = Test { getChild :: child }
type family Child t
type instance Child (Test child) = child
-- This compiles
test :: HasFoo child => Test child -> ()
test t = foo (getChild t)
-- This compiles as well
type TestCtx t = HasFoo (Child t)
test2 :: TestCtx (Test child) => Test child -> ()
test2 t = foo (getChild t)
EDIT2
Actually, it does not work WHEN COMBINED WITH ASSOCIATED TYPES. It works in some cases where associated types are not used - jump to my next comment below to see an example working code. In such a case, I suspect this is some kind of type inference bug, isn't it?
Guys, there is yet another problem with the syntax of trait aliases, and I would consider it pretty serious. Basically, as I mentioned earlier, trait aliases and GATs solve 2 of the biggest (in my opinion) current problems of Rust – lack of possibility of code generalization. GATs allow us to generalize &
and &mut
in a nice way (more about it here: https://github.com/rust-lang/rust/issues/44265#issuecomment-544320238), while trait aliases allow us to create aliases for multiple trait bounds. This is huge. Basically, when designing a complex generic system, we don't want to copy-paste a lot of bounds everywhere. We should give them a name and use the name everywhere instead. If our original implementation changes, we would only need to update the alias, not every usage of it.
However, the syntax when used with functions currently sucks. Here is an example from my codebase:
////// Definition //////
pub trait ItemDirtyCtx<Ix, OnSet> = where
Ix: Unsigned + Debug,
OnSet: Callback0 + Debug;
////// Usage //////
pub type BufferOnSet<Ix, OnDirty> = impl Fn(usize);
fn buffer_on_set<Ix, OnDirty> (dirty: &ItemDirty<OnDirty>) -> BufferOnSet<Ix, OnDirty>
where (): ItemDirtyCtx<Ix, OnDirty> {
let dirty = dirty.clone();
move |ix| dirty.set(ix)
}
The thing to note here is this line (warning! ugly!):
where (): ItemDirtyCtx<Ix, OnDirty> {
Basically it is a way to tell Rust "use the ItemDirtyCtx
alias and apply several bounds to Ix
and to OnDirty
". The ability to do it, to name these several bounds in a variable ItemDirtyCtx
is huge, is necessary, is amazing. But the only available syntax to use it is rather ugly.
Side note: Of course, I could arbitrarily choose Ix
or OnDirty
as Self
in trait alias and re-write it to Ix: ItemDirtyCtx<OnDirty>
or OnDirty: ItemDirtyCtx<Ix>
, but this would introduce serious confusion to the reader, as it would suggest that ItemDirtyCtx
is something bound to one of the types.
Fortunately, the fix will be pretty simple. Why don't we allow the following syntax for using trait aliases in the where
clause then?
fn buffer_on_set<Ix, OnDirty> (dirty: &ItemDirty<OnDirty>) -> BufferOnSet<Ix, OnDirty>
where ItemDirtyCtx<Ix, OnDirty> {
let dirty = dirty.clone();
move |ix| dirty.set(ix)
}
Your use-case still implies ItemDirtyCtx
to be implemented for ()
. How should where Trait
be re-interpreted? As where (): Trait
? I think it’s a pretty non-common situation, or maybe there’s something I just didn’t get about your snippet.
@phaazon My use case doesn't really care on which ItemDirtyCtx
is implemented on. The idea here is to create an alias for multiple trait bounds. It is an alias for trait bounds of Ix
and OnDirty
. This is a much-simplified code. Often I've got like 4 or 5 different type parameters and I don't want to copy all trait bounds for each type separately. Instead, I'm creating a single alias and using it where necessary.
What you describe is trait Alias = Ix + OnDirty
, no?
@phaazon No! :)
The code before refactoring looked like this:
fn func1<Ix, OnDirty> (args: Args) -> Output
where Ix: Unsigned + Debug,
OnSet: Callback0 + Debug {
body
}
// possibly different module or different trait impl:
fn func2<Ix, OnDirty> (args: Args2) -> Output 2
where Ix: Unsigned + Debug,
OnSet: Callback0 + Debug {
body2
}
Using trait aliases I can introduce alias to multiple trait bounds on different types. So after introducing
pub trait ItemDirtyCtx<Ix, OnSet> = where
Ix: Unsigned + Debug,
OnSet: Callback0 + Debug;
I can now refactor that code to:
fn func1<Ix, OnDirty> (args: Args) -> Output
where (): ItemDirtyCtx<Ix, OnSet> {
body
}
// possibly different module or different trait impl:
fn func2<Ix, OnDirty> (args: Args2) -> Output 2
where (): ItemDirtyCtx<Ix, OnSet> {
body2
}
The value of that becomes even more visible when you've got more type parameters than two and you want to keep your codebase tidy and well-named. Then you want to name several different trait bounds on different types using a single alias. You cannot use trait Alias = Ix + OnDirty
to express that. Does it make more sense now?
I would call that a "bound alias", not a "trait alias". I agree bound aliases are useful but these seem like distinct concepts. Sure, you can "hack" a bound alias using trait aliases and ignoring Self
, but that feels like a case of having a hammer and seeing nails.
Yeah. This seems to generally fall under the ambit of "constraint kinds", which there has been some discussion of in the past. They're more powerful, though I know not everyone is in favour of them. They'd probably need to wait on Chalk too...
@RalfJung I completely agree that bound aliases
with better syntax might be better here (though, I'm not sure we may want to have separate concepts for trait aliases with explicit Self and without explicit Self). Using tuple here is a hack, feels like a hack, but it is currently the only solution allowing us to create bound aliases, which are crucial for high code quality in bigger projects.
We should think here if bound aliases should be a separate concept, or in fact, this is a sub-concept of trait aliases. In the latter case, we may want to deliver a special syntax for trait aliases without Self. In fact, even the RFC docs mention such usage patterns allowing us to define trait aliases as trait alias = where ...
.
@alexreg constraint kinds are much more powerful. Every nested tuple is a constraint in Haskell, which allows you to create type-level functions that iterate over constraints and allow you to modify them. This is an amazing power. Using aliases for constraints does not require constraint kinds, at least in Haskell.
We should think here if bound aliases should be a separate concept, or in fact, this is a sub-concept of trait aliases.
This is actually one of the concerns that has been raised about this feature in the past: there are three different interpretations of "trait aliases" (i.e. aliases for traits, aliases for bounds and aliases for constraints) and it's not entirely clear which of the three we want (or all of them).
@wdanilo First, the discussion over what to call them has been had to death. "Trait aliases" is the placeholder name.
Also, why are you telling me about constraint kinds haha? I know they're much more powerful. In fact, I just said that.
@alexreg when talking on a public channel, like here, I prefer to clarify things to readers not familiar with the concepts. You mentioned that they are more powerful, and I provided an additional explanation of why. In no means my message was suggesting that you don't know it, sorry if you felt so! :)
The value of that becomes even more visible when you've got more type parameters than two and you want to keep your codebase tidy and well-named. Then you want to name several different trait bounds on different types using a single alias. You cannot use
trait Alias = Ix + OnDirty
to express that. Does it make more sense now?
Yep, I got you. I got that need too. Thanks for clarifying.
@wdanilo No problem, just a miscommunication then... I appreciate you elaborating!
Is this feature accepting feedback yet?
I've been playing around with it, and it doesn't work at all with associated types and closures.
Am I doing something wrong?
EDIT:
I looked into it, and it seems like it's just an issue with HRTB.
I'd like to propose a syntax for what's referred to as "bound aliases" above. Instead of this:
trait Foo<A, B> = where ...
The syntax could look like this:
where Foo<Self, A, B, ...> = ...
Where, you may ask, did Self
come from? It's there because every trait has an implicit type parameter named Self
, but a bound alias may or may not have a Self
parameter. Just as a method's self
parameter comes from the expression before the .
when the method is called, a trait's Self
parameter comes from the type expression before the :
when it appears in a bound.
Above, @wdanilo gave an example of a bound alias, ItemDirtyCtx
, that does not use the Self
parameter, so it appears with a dummy Self
type in a where
clause: where (): ItemDirtyCtx<...>
. This is ugly and confusing, because it's not obvious from the declaration of ItemDirtyCtx
that it's meant to be used that way, and at the point of use, ()
may be replaced by any type at all without changing the meaning!
In my proposal, a bound alias like ItemDirtyCtx
would be written without a Self
parameter, and it would appear on its own in a where
clause, with no :
and no dummy type. The rule would be that when the alias appears in a bound, it must appear after a :
if it has a Self
parameter, and it must appear without a :
if it doesn't.
(Also, if you'll indulge me in a bit of meta-bikeshedding, I don't think "bound alias" is a good name, because it's not an alias any more than a function is an "expression alias". Maybe "bound constructor" would be be more appropriate?)
Just a simple question:
#![feature(trait_alias)]
trait Foo = ;
(playground link)
is this supposed to parse and compile without any error nor warning ? 🤔
Without the feature, these are already accepted today:
fn a<T>() where T: {}
fn b<T:>() {}
With the feature, I guess it's like writing:
#![feature(trait_alias)]
trait Foo = ;
fn a<T>() where T: Foo {}
fn b<T: Foo>() {}
Although not very useful, it appears consistent.
Oh okay, thank you for the explanation :)
Yes, IIRC, trait bounds can be null. And it’s actually a pretty good feature I’m happy exists, especially when generating bounds in macro with $(… +)*
:smile:
I stumbled across a case where i'd like to use this feature. Is it on the roadmap for stabilization?
Most helpful comment
I stumbled across a case where i'd like to use this feature. Is it on the roadmap for stabilization?