Hello,
What's the difference between:
trait Foo<T> {
}
and
trait Foo {
type T
}
I see they can be instantiated with Foo<MyType>
and Foo<T=MyType>
, respectively.
Thanks!
Best, Oliver
Hello,
What's the difference between:
trait Foo<T> {
}
and
trait Foo {
type T
}
I see they can be instantiated with Foo<MyType>
and Foo<T=MyType>
, respectively.
Thanks!
Best, Oliver
With a generic parameter, Foo<String>
and Foo<i32>
are two different traits, and a type may implement one of them or both (or neither). With an associated type, there is only a single trait Foo
, and a type can only implement Foo
once with a single choice for the type.
Or, to put it differently:
As far as I understand, anything that can be achieved with an associated type can be achieved by using a type parameter and only providing a single implementation (assuming there are no other type parameters). Insofar, using an associated type is:
It's not just syntactic sugar. The guarantee has consequences, for once it's enforced. (The system that enforces the single-implementation restriction alone is enought not to consider it "syntactic sugar".) Also, an associated type can be named in terms of the Self
-type (and the other type parameters), so that you save the need for additional type parameters in generic functions working with the trait, and more importantly also structs containing the type. E.g.
trait Trait1<T> {}
trait Trait2 {
type T;
}
struct Foo<S: Trait1<T>, T> {
x: S,
y: T,
}
struct Bar<S: Trait2> {
x: S,
y: S::T,
}
Finally, associated types support trait bounds that are implied at the use-site. If you have a trait Foo<T: Bound>
, you'll be forced to repeat the bound T: Bound
everywhere you use Foo<T>
, whereas trait Bar { type T: Bound; }
doesn't have that problem.
To quote a very old RFC:
trait Generics<A, B, C, D, E> {
fn other_stuff();
}
trait Assocs {
type A;
type B;
type C;
type D;
type E;
fn other_stuff();
}
fn call_other_stuff_generics<A, B, C, D, E, T> ()
where
T : Generics<A, B, C, D, E>,
{
T::other_stuff()
}
fn call_other_stuff_assocs<T : Assocs> ()
{
T::other_stuff()
}
As well as:
I meant consequences beyond that, e.g. does it enable you do do something that can't be done without the enforcement (other than writing shorter code)?
That also holds for the type parameters. Inside the definition, I can use their name; outside the definition, I can use their name too, as I have to provide a type name as well (which might be a different name, of course). The name isn't fixed, but I could fix it by convention (which is why I said it's syntactic sugar).
That's also just shortening the amount of code to write.
Arguably, having generics in the first place also is just "shortening the amount of code to write". You could also just write all the monomorphized instances by hand or use macros.
Quoting @Yandros's quote:
In a way that is "something that can't be done" (without associated types).
Hmmmmmmm… Everything is just syntactic sugar for writing binary machine code
(But good point anyway.)
Associated types allow you to express stuff as type-level functions, being able to, with a non-generic implementor of a trait, query its associated type.
For instance, if you had:
trait Trait<GenericInput> {}
and you knew that there were a single impl Trait<Something> for String {}
, but where you don't know which Something
it is —this can happen for a variety of reasons; the main one being that the impl may come from an external crate which may change it, and you wan't to reduce maintenance if bumping semver; it may also come from your own crate but within code written by a colleague / coworker or even by yourself and you may have forgotten, and last but definitely not least: String
may actually be a type given to a macro, and "you" are the macro author trying to interact with Something
.
One cannot name a type out of "unique existential-ness", so Something
can never be queried. While you may sometimes circumvent the issue using a helper "overly" generic context, and then letting type inference fill the Something
, that cannot always be applied.
That is, one cannot write:
type OnlyInputOfTrait<T>
where
∃!<U> T : Trait<U>,
= U;
(which would be the way to get Something
above: OnlyInputOfTrait<String>
).
The whole design of:
for instance, relies on being able to express the C layout of a struct with a defined C layout at its level (#[repr(C)]
) and with each field recursively having a C layout. With that, it can define a type with public and equivalent C layout to that of the definition:
#[derive_ReprC]
#[repr(C)]
pub struct Point {
x: f32,
y: f32,
}
becomes:
#[repr(C)]
pub struct Point {
x: f32,
y: f32,
}
#[repr(C)]
pub struct Point_Layout {
pub x: <f32 as ReprC>::CLayout,
pub y: <f32 as ReprC>::CLayout,
}
impl ReprC for Point {
type CLayout = Point_Layout;
…
}
The definition of Point_Layout
is simply impossible to write without associated types.
"shorter code" is also hoisted / modularized / compartimentalized code, which makes it way easier to have moving parts interact independently and add changes without the other party necessarily knowing about it.
Considering a sealed (and #[dyn_safe(false)]
) trait, for instance, one can add a new associated type to it, without breaking downstream code. It is generally not possible to do the same with generic type parameters only.
Here's an important consequence: You can use associated types in situation where you simply can't just add another to-be-inferred type parameter. For example when implementing a trait. While it might be fine for your personal function to be fn foo<S: Trait<T>, T>()
, instead of fn foo<S: Trait>()
, if you want to write an implementation impl Foo for S where S: Trait
of a trait trait Foo { fn foo(); }
, then you cannot implement this same trait anymore without the associated type.
Considering a sealed trait, for instance, one can add a new associated type to it, without breaking downstream code
Strictly speaking, that's not always right, AFAIK, if the trait is used as a trait object, since one must always set values for all associated types of dyn Trait
.
Ah good, catch, lemme add a #[dyn_safe(false)]
annotation there
Associated types allow you to express stuff as type-level functions, […]
I think I understand (but still need to digest the rest of your posting). You can't "retrieve" the associated type (at compile time) in any way, which is a problem when attempting to do what @steffahn said:
[…] if you want to write an implementation
impl Foo for S where S: Trait
of a traittrait Foo { fn foo(); }
, then you cannot implement this same trait anymore without the associated type.
I cannot write: impl Foo for S where S: Trait<_>
, as I won't know what _
is in each case.
(Hope I got that right.)
I didn't know that before. Thanks!
As far as I understand, anything that can be achieved with an associated type can be achieved by using a type parameter and only providing a single implementation (assuming there are no other type parameters).
You can do these today:
fn f() -> impl Iterator {
"".chars()
}
fn g() -> impl Iterator<Item = impl Display> {
"".chars()
}
Where as this
trait IteratorWithInputItem<Item> { ... }
fn h() -> impl IteratorWithInputItem {
"".chars()
}
Results in error[E0107]
: missing generics for trait IteratorWithInputItem
. And this:
fn i() -> impl IteratorWithInputItem<impl Display> {
"".chars()
}
Results in error[E0666]
(): nested
impl Trait
is not allowed.
You can also do
trait Foo {}
impl<T, U> Foo for T where T: Iterator<Item=U> {}
while you cannot instead do
impl<T, U> Foo for T where T: IteratorWithItemInput<U> {}
error[E0207]
: the type parameter U
is not constrained by the impl trait, self type, or predicates
Playground. This is a result of RFC 447 and I believe it has further implications for well-formedness via RFC 1214.
- some sort of "guarantee" there is just one such implementation (not sure if this guarantee has any consequences though; I don't think it has).
The above examples are some consequences. There's probably others related to coherence ala this thread and tricks like this, but I'll stop digging for now.
I cannot write:
impl Foo for S where S: Trait<_>
, as I won't know what_
is in each case.
Hmm, you gave an interesting syntax, that I also had in mind but thought wouldn't work:
[…] you cannot instead do
impl<T, U> Foo for T where T: IteratorWithItemInput<U> {}
error[E0207]
: the type parameterU
is not constrained by the impl trait, self type, or predicatesPlayground. This is a result of RFC 447 and I believe it has further implications for well-formedness via RFC 1214.
In a way it could work (even if it doesn't), because U is (indirectly) restricted due to being mentioned in the where
clause.
I remember you mentioned the requirement to constrain type parameters somehow (as demanded by RFC 447), and I just searched the history and found your message here in "Weird error with tokio::try_join! and mapped futures".
As far as I understand, RFC 447 came after associated types were introduced. So maybe allowing the syntax impl<T, U> …
could have been a (worse) alternative to the associated types? Maybe it's more difficult to implement a compiler allowing these things (and stuff like nested impl
s).
Let me mention Haskell. They have a feature called “functional dependencies” which lets you specify (explicitly) that one type argument of a trait is determined by a set of others. Porting their syntax to Rust, this could look like
trait Foo<T> | Self -> T {
// ...
}
where the Self -> T
means that T
is fully determined by Self
.
Instead of something like
trait Bar<B> {
type X;
type Y;
}
you could work with
trait Bar<B, X, Y> | Self B -> X Y {
}
(the Self B -> X Y
would mean that both X
and Y
are uniquely determined by Self
and B
combined.
Haskell also has associated types, but that language feature is younger. It’s commonly known around Haskellers that functional dependencies and associated types can solve pretty-much the same kind of problems, but with different syntax (each has some syntactical pros and cons).
(One of the syntactical disadvantages is related to the fact that Haskell doesn’t have an equivalent to the Rust-syntax used in e.g. where I: Iterator<Item = T>
. In Haskell, you’d need to write about as much as the equivalent of where I: Iterator, <I as Iterator>::Item == T
, roughly speaking. If you have more arguments and types, that can get lengthy, e.g. T: Foo<A, B, Ty1 = C, Ty2 = D>
vs T: Foo, <T as Foo<A, B>>::Ty1 == C, <T as Foo<A, B>>::Ty2 == D
. While with fun-deps you just have T: Foo<A, B, C, D>
. Another advantage of functional dependencies is that they’re slightly more flexible; you can specify complicated things like A
depends on B
and C
depends on B
and D
combined ony a single trait, “B -> A, B D -> C
”. Trait implementations also become more concise, because you can save the need for separate lines for the type
s and also they don’t have names. Unlike Haskell, Rust loves lengthy trait implementations though; ever noticed that re-stating the method’s type signatures on every impl
is insanely redundant? So it’s less of a point for Rust
, I guess.)
This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.