Sometimes one wants to explicitly make a lifetime parameter of a struct
invariant, meaning that it allows no subtyping at all. For example, this is needed when using the “generative lifetime” trick, and sometimes it is also done for future-compatibility reasons. However, since variance is inferred in Rust, expressing invariance can get quite awkward. A common pattern I have seen is to add
_marker: PhantomData<&'a mut &'a ()> // the T in &mut T is invariant
or
_marker: PhantomData<fn(&'a ()) -> &'a ()> // both covariant and contravariant occurrence -> invariant
or
_marker: PhantomData<Cell<&'a ()>> // the T in Cell<T> is invariant (this also kills `Sync`)
but all of these are far from self-explaining. It turns out even seasoned Rust programmers can get this wrong (in that case &'a mut ()
was used, which however does not make 'a
invariant).
(EDIT: Turns out I misinterpreted their intent, and the lifetime was not meant to be invariant.)
So I propose that we add two marker types to the standard library:
pub struct Invariant<T: ?Sized>(pub PhantomData<fn(Box<T>) -> Box<T>);
pub struct InvariantLifetime<'a>(pub Invariant<&'a ()>);
// both have trivial constructors
that can be used in situations like the above. I have not seen an example where a type parameter (as opposed to a lifetime parameter) needs to be invariant, but that is an obvious extension so I included it.
The generative-lifetime-based bounds check already defines a type Id
for this purpose, demonstrating that this kind of marker type is useful. It (a) removes the risk of accidentally having the wrong variance due to a typo, and (b) expresses the intent much more clearly and hence does not need a comment explaining how this achieves invariance.
Other languages such as Scala have explicit variance annotations. Rust decided against that. This proposal does not change that decision, but it provides a way for programmers to more explicitly express variance when needed without requiring new language features.
An obvious future extension of this proposal is to also have Covariant
and Contravariant
types, but I have not yet seen an example where one would actually want that. Also, adding a _marker: Covarant<T>
does not mean the type is actually covariant, it just means it is no more than covariant:
struct WhatIsThis<T> {
_marker1: Covariant<T>,
_marker2: Contravariant<T>,
}
This type is neither covariant nor contravariant – it is actually invariant. Covariant
sounds like it makes a
positive statement (“this type is covariant”), which is not something we can realize with marker types (that can only add new constraints). In contrast, Invariant
is actually a negative statement (“this type is neither covariant nor contravariant”) and as such can be realized by marker types. So, Covariant
and Contravariant
marker types have a much more confusing semantics than Invariant
and they seem to be much less needed. Hence I think we should not add them at this point.
What do you think, does this make sense? Is the standard library the right place for such marker types?