The problem
Given a trait:
trait Trait {
type Assoc<'a, T, U>;
}
In the above, <Self as Trait>::Assoc<'a, T, U>
is invariant with respect to Self
, 'a
, T
, and U
.
Frustratingly, today's Rust provides no way to specify a different variance. This limits the language's expressiveness. For example, it makes it certain GAT abstractions much less ergonomic:
It also makes it effectively impossible to implement generic data structures that store enum discriminants or pointer metadata, while properly encapsulating that implementation detail:
An explicit syntax for requiring associated types to have a particular variance would alleviate all of these issues.
Propsed solution
There is lost of bikeshedding in the first thread regarding syntax for this feature, but I'd like to propose another option: a new type of where-clause bound. For example:
trait Trait {
type Assoc<'a, T, U>: covariant(Self) + contravariant(T) + covariant(U) + contravariant(U);
}
In the above, <Self as Trait>::Assoc<'a, T, U>
is covariant with respect to Self
, invariant with respect to 'a
, contravariant with respect to T
, and bivariant with respect to U
. Any implementation of Trait
must uphold these guarantees:
impl<'s> Trait for &'s () {
// Following is a valid implementation, meets all the variance bounds.
type Assoc<'a, T, U> = (Self, &'a mut u32, fn(T));
}
impl Trait for i32 {
// Following is also a valid implementation, meets all the variance bounds.
type Assoc<'a, T, U> = ();
}
/*
impl<'s> Trait for &'s u32 {
// ERROR: the bound `covariant(Self)` is not satisfied
type Assoc<'a, T, U> = (&'s mut Self, &'a mut u32, fn(T));
}
*/
Variance bounds outside the trait definition
An advantage of type bounds is that they are usable in where clauses outside the trait definition. For example, one can write:
trait Foo {
type Assoc;
}
fn bar<T: Foo>(&T) -> <T as Foo>::Assoc where T::Assoc: covariant(T) {
// ...
}
// This struct is covariant with respect to `T`
struct Baz<T>
where
T: Foo,
<T as Foo>::Assoc: covariant(T),
{
inner: <T as Foo>::Assoc
}
In addition to being more flexible than attributes on the type parameters, trait bounds can be relegated to the where
clause, alongside all the other complicated type bounds. This helps keep code and documentation approachable.
Restrictions on variance bounds
For simplicity, I would suggest that variance bounds be restricted in the following ways, at least initially:
- They can restrict only the associated types of traits
- No matter where a variance bound appears, its type parameter (the
T
incovariant(T)
) must be one of:- The
Self
type of the trait - A GAT type parameter of the associated type
- The
Conclusion
Variance is infamously hard to grasp. In part because of this, the Rust language tries hard to make it implicit and hide it from the programmer. Most of the time, this makes sense. But when you do need explicit control, Rust doesn't provide it. Things that should work become impossible, and give error messages that only confuse you more. Explicit variance annotations for the few cases where they are necessary would make the language more ergonomic and easier to use.