In Scala and most other languages that care about variance, objects live on the heap and the only thing “casting” does is telling the complier what methods can be called on a given pointer.
In Rust it’s not that simple:
&Trait have different size (“fat pointers”), so Rust has to do additional transformations when you’re casting
&Trait on the stack (you don’t have to write special take methods for that, btw, just a cast/assignment works), and you can’t just cast/assign
Foo<&Trait> because that’d require rewriting the object (or rather creating a new one, because it may have a different size…) to change all the references to
T it has.
This is why, variance-wise,
&T is not a subtype of
&Trait. Instead, there is some special unstable unsizing magic in a form of a few traits that lets the type such as
Box to opt-in into this “recreate with internal references swapped for fat references” coercion (in its current form it only supports having one such reference).
The same can be said about other conversions/coercions, e.g. you can cast
35u32 as u64, but you can’t just treat
Foo<u64>, although I doubt there will ever be any traits or language support to enable that.
So, back to variance, the only subtyping relation in Rust is
'a: 'b and
Foo<'c>: Foo<'d>, because lifetimes don’t influence the ABI. If the ABI is guaranteed to be the same for unique and shared references to sized types, the subtyping relations could be extended to allow casting from
Foo<&mut T> to
Foo is covariant (anyone feels like writing an RFC?).
P.S. not sure this is relevant for your explorations, but
&mut T is only covariant over
T: !Sized (because then you can’t “write” another object through it, just modify the existing one), otherwise it’s invariant.