Type parameter versus type alias in trait

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.

8 Likes

Or, to put it differently:

  • A type parameter, just like a function parameter, is an input to the trait.
  • An associated type is an output type: it is determined by the implementation of the trait itself.
9 Likes

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:

  • syntactic sugar for the case you only have one implementation,
  • 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).

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.

7 Likes

To quote a very old RFC:

An example
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:

  • Note that sometimes type inference can be too smart for its own good, so the "importing a crate could break existing code" is a real issue in current Rust.
2 Likes

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.

3 Likes

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 :rofl:

(But good point anyway.)

1 Like

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.

3 Likes

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.

2 Likes

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.

3 Likes

Ah good, catch, lemme add a #[dyn_safe(false)] annotation there

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:

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!

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](:scream:): nested impl Trait is not allowed.

Playground.


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.


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.

2 Likes

Hmm, you gave an interesting syntax, that I also had in mind but thought wouldn't work:

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

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

2 Likes

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.