rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
96.87k stars 12.52k forks source link

Tracking issue for trait aliases #41517

Open withoutboats opened 7 years ago

withoutboats commented 7 years ago

This is a tracking issue for trait aliases (rust-lang/rfcs#1733).

TODO:

Unresolved questions:

durka commented 7 years ago

I think #24010 (allowing aliases to set associated types) should be mentioned here.

durka commented 6 years ago

I'd like to take a crack at this (starting with parsing).

carllerche commented 6 years ago

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.

hadronized commented 6 years ago

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?

carllerche commented 6 years ago

@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.

hadronized commented 6 years ago

I see. Cannot we just use free variables for that? Like, Service<Request = http::Request> implies the free variable used in http::request<_>?

carllerche commented 6 years ago

@phaazon I don't understand this proposal.

varkor commented 6 years ago

@durka: how's the work on the follow-up to https://github.com/rust-lang/rust/pull/45047 going?

clarfonthey commented 6 years ago

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.

durka commented 6 years ago

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 .

clarfonthey commented 6 years ago

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.

petrochenkov commented 6 years ago

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.

clarfonthey commented 6 years ago

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.

clarfonthey commented 6 years ago

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.

alexreg commented 6 years ago

@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.)

Nemo157 commented 6 years ago

One thing I don't see mentioned in the RFC is whether useing trait aliases brings methods into scope? (see the next comment for a correct example, mine was wrong).

pengowen123 commented 6 years ago

@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();
}
seanmonstar commented 6 years ago

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:

If specific issues could also be listed, it'd help keep track of the work remaining.

durka commented 6 years ago

@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.

alexreg commented 6 years ago

@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.

varkor commented 6 years ago

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.

alexreg commented 6 years ago

@varkor Fair point. I agree bounds would be a more accurate name, and lead to less confusion.

rpjohnst commented 6 years ago

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.

varkor commented 6 years ago

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.

clarfonthey commented 6 years ago

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.

varkor commented 6 years ago

@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.).

clarfonthey commented 6 years ago

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.

varkor commented 6 years ago

@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.)

glandium commented 6 years ago

anecdata, but I would find bounds to be more confusing than trait.

durka commented 6 years ago

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.

varkor commented 6 years ago

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.

withoutboats commented 6 years ago

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.

hadronized commented 6 years ago

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:

varkor commented 6 years ago

@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.

varkor commented 6 years ago

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.

alexreg commented 6 years ago

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.)

jplatte commented 6 years ago

@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 commented 6 years ago

@alexreg Your second code block should probably be

Yep, copy & paste coding fail, thanks for pointing it out! I fixed it now.

alexreg commented 6 years ago

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.

hadronized commented 6 years ago

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();
  // …
}
hadronized commented 6 years ago

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>
RalfJung commented 6 years ago

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.

Centril commented 6 years ago

@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.

Nemo157 commented 5 years ago

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?

alexreg commented 5 years ago

@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.

alexreg commented 5 years ago

@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. :-)

alexreg commented 5 years ago

@Nemo157 Actually, would you mind opening a separate issue about that, which references this tracking issue?

Nemo157 commented 5 years ago

@alexreg opened #56485

ricochet1k commented 5 years ago

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.

alexreg commented 5 years ago

@ricochet1k Best to file as an issue on the rustfmt repo. Thanks! https://github.com/rust-lang/rustfmt/issues