Gankro gave an example, but here's a better way to think about associated types:
Think about them the same way you think of a method. Methods are "associated functions". If you implement a trait on a type, it can only have one version of a trait method, not two. Similarly, it can have one associated type, not multiple.
With a generic trait the trait itself is generic; there are multiple "versions" of this trait so you can implement it multiple times for different parameters and different methods.
A concrete way to think about this is overloaded methods. Rust doesn't have the regular kind of overloading, but it does have the Fn traits.
trait FnOnce<Args> {
type Output;
fn call_once(self, args: Args);
}
Note that it has both a type parameter and an associated type.
Now, we might have a type Foo which is `FnOnce<(u8,),Output=bool>`. This means that it takes a single integer in, and outputs a bool. We can overload it by also implementing `FnOnce<(char,), Output=bool>` and `FnOnce<(u8,u8),Output=char>`. This means that it can also take in a char and return a bool, or take two integers and output a char.
Now, given these impls, I can try to overload it with `FnOnce<(u8,u8),Output=bool>`. However, we can't. Because this means that the function will return either a book or a char when you feed it two ints. This is not how we want functions to behave, and this is why Output is an associated type. For a given trait implemented on a given object (in this case FnOnce<(u8,u8)> implemented on Foo), there is only one output type. But there can be multiple versions of the trait implemented by changing the generic part. So, while an overloaded function may take in multiple different input types, each set of inputs has only one possible output type.
Of course, Rust doesn't forbid having it the other way around -- we could make Output a generic parameter too, and have overloaded functions which can have different output types for the same input (and need type annotations to choose). But we don't want FnOnce to work that way, so we don't have it like that.
Similarly, given an iterator, there is only one type it can produce. We don't want there to be types which are iterators over multiple things.
On the other hand, the trait PartialEq has a single type parameter. This is because we want you to be able to test equality between many types.
> Of course, Rust doesn't forbid having it the other way around -- we could make Output a generic parameter too, and have overloaded functions which can have different output types for the same input (and need type annotations to choose). But we don't want FnOnce to work that way, so we don't have it like that.
Can you please explain why FnOnce shouldn't work this way?
Because overloading the return type (for the same inputs) is pretty unusual :)
Rust has the machinery to deal with this -- you'd need type annotations -- it just gets annoying.
Specific functions in Rust do get "overloaded" by return type by returning a generic parameter (e.g. the collect() method on iterators), but in general functions are expected to work without needing type annotations on return.
I mean, allowing this kind of use of FnOnce isn't a bad idea. I don't see major issues with it, just minor ones. That's not the choice that was made during the design. I'm sure if we looked at the original design rfcs this point will have come up somewhere :)
I wasn't involved in that decision, so I'm not aware of the exact reasoning.
Exactly -- it only stops you from creating function-likes that have multiple return types (not closures, because closures aren't locally generic anyway). If you really need such behavior, regular generic functions or traits will work, e.g. `.collect()`
Think about them the same way you think of a method. Methods are "associated functions". If you implement a trait on a type, it can only have one version of a trait method, not two. Similarly, it can have one associated type, not multiple.
With a generic trait the trait itself is generic; there are multiple "versions" of this trait so you can implement it multiple times for different parameters and different methods.
A concrete way to think about this is overloaded methods. Rust doesn't have the regular kind of overloading, but it does have the Fn traits.
https://doc.rust-lang.org/core/ops/trait.FnOnce.html
(ignore the rust-call stuff)
The trait is essentially:
Note that it has both a type parameter and an associated type.Now, we might have a type Foo which is `FnOnce<(u8,),Output=bool>`. This means that it takes a single integer in, and outputs a bool. We can overload it by also implementing `FnOnce<(char,), Output=bool>` and `FnOnce<(u8,u8),Output=char>`. This means that it can also take in a char and return a bool, or take two integers and output a char.
Now, given these impls, I can try to overload it with `FnOnce<(u8,u8),Output=bool>`. However, we can't. Because this means that the function will return either a book or a char when you feed it two ints. This is not how we want functions to behave, and this is why Output is an associated type. For a given trait implemented on a given object (in this case FnOnce<(u8,u8)> implemented on Foo), there is only one output type. But there can be multiple versions of the trait implemented by changing the generic part. So, while an overloaded function may take in multiple different input types, each set of inputs has only one possible output type.
Of course, Rust doesn't forbid having it the other way around -- we could make Output a generic parameter too, and have overloaded functions which can have different output types for the same input (and need type annotations to choose). But we don't want FnOnce to work that way, so we don't have it like that.
Similarly, given an iterator, there is only one type it can produce. We don't want there to be types which are iterators over multiple things.
On the other hand, the trait PartialEq has a single type parameter. This is because we want you to be able to test equality between many types.