rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
98.33k stars 12.72k 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:

topecongiro commented 5 years ago

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

alexreg commented 5 years ago

@topecongiro Definitely an oversight. Thanks for the report.

seanmonstar commented 5 years ago

Hit another ICE today reported in https://github.com/rust-lang/rust/issues/59029

davidbarsky commented 5 years ago

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?

alexreg commented 5 years ago

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

davidbarsky commented 5 years ago

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

varkor commented 5 years ago

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

Centril commented 5 years ago

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.

seanmonstar commented 5 years ago

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

Centril commented 5 years ago

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

seanmonstar commented 5 years ago

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

Centril commented 5 years ago

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

seanmonstar commented 5 years ago

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?

Centril commented 5 years ago

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

SimonSapin commented 5 years ago

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

Centril commented 5 years ago

Are they on any roadmap?

Nope.

is that your personal opinion?

Yep.

varkor commented 5 years ago

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

vi commented 5 years ago

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?

varkor commented 5 years ago

@vi: the existing concerns are about the entire feature, rather than particular details (e.g. the current syntax may not be appropriate).

alexreg commented 5 years ago

@Centril FYI added an unresolved question for the matter of maybe bounds in trait objects.

wdanilo commented 5 years ago

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:


rust-lang/rfcs#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?

wdanilo commented 5 years ago

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)
}
hadronized commented 5 years ago

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.

wdanilo commented 5 years ago

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

hadronized commented 5 years ago

What you describe is trait Alias = Ix + OnDirty, no?

wdanilo commented 5 years ago

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

RalfJung commented 5 years ago

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.

alexreg commented 5 years ago

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

wdanilo commented 5 years ago

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

varkor commented 5 years ago

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

alexreg commented 5 years ago

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

wdanilo commented 5 years ago

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

hadronized commented 5 years ago

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.

alexreg commented 5 years ago

@wdanilo No problem, just a miscommunication then... I appreciate you elaborating!

Jezza commented 4 years ago

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.

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=d356124710c101f92436fdcfb245fa9b

Am I doing something wrong?

EDIT: I looked into it, and it seems like it's just an issue with HRTB.

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=d596d3aa7c0ac62fe151813ecd91f4d0

johnw42 commented 4 years ago

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

shika-blyat commented 4 years ago

Just a simple question:

#![feature(trait_alias)]
trait Foo = ;

(playground link) is this supposed to parse and compile without any error nor warning ? 🤔

memoryruins commented 4 years ago

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.

shika-blyat commented 4 years ago

Oh okay, thank you for the explanation :)

hadronized commented 4 years ago

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:

reuvenpo commented 4 years ago

I stumbled across a case where i'd like to use this feature. Is it on the roadmap for stabilization?

aidanhs commented 3 years ago

It looks like Fn trait aliases do not help closure argument type inference, whereas the original trait does:

#![feature(trait_alias)]

trait MyFn = Fn(D);

struct D;
impl D {
    fn x(&self) {}
}

//fn add(_: impl Fn(D)) {} // yep!
fn add(_: impl MyFn) {} // no!

fn main() {
    add(|x| { x.x() });
}

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=e637bc855969fbac3c6f75c60d0070a1

SimonSapin commented 3 years ago

@rust-lang/lang Does this feature seem ready enough to use in public standard library APIs?

https://github.com/rust-lang/rfcs/pull/2580 proposes adding pub trait Thin = Pointee<Metadata = ()>; (with Pointee a trait also added) and using it in std::ptr::null, replacing the implied T: Sized bound with T: ?Sized + Thin (in order to extend it to extern types). Without trait aliases, that bound could be T: ?Sized + Pointee<Metadata = ()>.

Is there an observable difference between the two formulations of the relaxed bound, in terms of which programs can compile or not and therefore of stability commitments?

I see that the issue description lists two unresolved questions but I’m lacking context and am not sure what they mean, and this thread is long. Adding links there would be helpful.

varkor commented 3 years ago

The unresolved questions mentioned in the issue are two minor problems compared to several of the unresolved questions that have been brought up in the thread since (insufficient flexibility, inconvenient syntax, misleading naming, and the debate about which of constraint/bound/type aliases are desirable).

My personal feeling is that it would be worth forming a working group to discuss these issues in more detail and plan out a roadmap to stabilisation. (I would be very happy to be involved with this if the lang team thinks it worth doing.)

SimonSapin commented 3 years ago

I’ve added those to the issue description, thanks!

Then let me turn my question around: if the bounds of ptr::null are relaxed from implicit T: Sized to T: ?Sized + Pointee<Metadata=()> (this is indeed relaxed because Pointee is implemented for every type), then later after we’ve figured out what trait aliases should look like, would it likely be backward-compatible to change it to T: ?Sized + Thin? It should be the same constraint on callers, just expressed differently.

varkor commented 3 years ago

@SimonSapin: yes, regardless of how the feature is eventually stabilised, the two ought to be interchangeable, so it's safe to use the explicit Pointee<Metadata = ()> for now and later replace it with a Thin trait alias.

lachlansneff commented 2 years ago

Has there been any further progress on this?

clarfonthey commented 2 years ago

I believe that the biggest blockers at the moment are syntax and semantics; do we want to have trait aliases or bounds aliases, and if so, should the syntax change at all? Honestly might even blocked on a future RFC solving these questions.

pnkfelix commented 2 years ago

Hey, the lang team just discussed this issue as part of a general "backlog bonanza", and our understanding is that this falls under the pervue of @rust-lang/wg-traits .

We noticed a number of unresolved questions in the issue description. I also see @clarfonthey 's recent comment.

So I think @rust-lang/lang wants a bit more detail on the blockers outlined there (preferably illustrated by concrete examples, if possible), as well as confirmation that those are the main things to be addressed.

So: can someone from @rust-lang/wg-traits provide a summary on the next steps here?

Kixunil commented 2 years ago

Yes please, make trait aliases also bound aliases. It'd be very practical.