Open withoutboats opened 7 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.
@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:
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?
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?
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() });
}
@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.
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.)
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.
@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.
Has there been any further progress on this?
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.
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?
Yes please, make trait aliases also bound aliases. It'd be very practical.
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?