In this question, an issue arose that could be solved by changing an attempt at using a generic type parameter into an associated type. That prompted the question "Why is an associated type more appropriate here?", which made me want to know more.
The RFC that introduced associated types says:
This RFC clarifies trait matching by:
- Treating all trait type parameters as input types, and
- Providing associated types, which are output types.
The RFC uses a graph structure as a motivating example, and this is also used in the documentation, but I'll admit to not fully appreciating the benefits of the the associated type version over the type-parameterized version. The primary thing is that the distance
method doesn't need to care about the Edge
type. This is nice, but seems a bit shallow of a reason for having associated types at all.
I've found associated types to be pretty intuitive to use in practice, but I find myself struggling when deciding where and when I should use them in my own API.
When writing code, when should I choose an associated type over a generic type parameter, and when should I do the opposite?
This is now described in the second edition of The Rust Programming Language. However, let's dive in a bit in addition.
Let us start with a simpler example.
There are multiple ways to provide late binding:
Or:
Disregarding any implementation/performance strategy, both excerpts above allow the user to specify in a dynamic manner how
hello_world
should behave.The one difference (semantically) is that the
trait
implementation guarantees that for a given typeT
implementing thetrait
,hello_world
will always have the same behavior whereas thestruct
implementation allows having a different behavior on a per instance basis.Whether using a method is appropriate or not depends on the usecase!
Similarly to the
trait
methods above, an associated type is a form of late binding (though it occurs at compilation), allowing the user of thetrait
to specify for a given instance which type to substitute. It is not the only way (thus the question):Or:
Are equivalent to the late binding of methods above:
Self
there is a singleReturn
associatedMyTrait
forSelf
for multipleReturn
Which form is more appropriate depends on whether it makes sense to enforce unicity or not. For example:
Deref
uses an associated type because without unicity the compiler would go mad during inferenceAdd
uses an associated type because its author thought that given the two arguments there would be a logical return typeAs you can see, while
Deref
is an obvious usecase (technical constraint), the case ofAdd
is less clear cut: maybe it would make sense fori32 + i32
to yield eitheri32
orComplex<i32>
depending on the context? Nonetheless, the author exercised its judgment and decided that overloading the return type for additions was unnecessary.My personal stance is that there is no right answer. Still, beyond the unicity argument, I would mention that associated types make using the trait easier as they decrease the number of parameters that have to be specified, so in case the benefits of the flexibility of using a regular trait parameter are not obvious, I suggest starting with an associated type.
Associated types are a grouping mechanism, so they should be used when it makes sense to group types together.
The
Graph
trait introduced in the documentation is an example of this. You want aGraph
to be generic, but once you have a specific kind ofGraph
, you don't want theNode
orEdge
types to vary anymore. A particularGraph
isn't going to want to vary those types within a single implementation, and in fact, wants them to always be the same. They're grouped together, or one might even say associated.