Subtyping and Variance
Before diving into each language, letβs establish the vocabulary.
- Subtyping $S <: T$ means a value of type $S$ can be used wherever a value of type $T$ is expected.
-
Variance describes how subtyping propagates through type constructors or composed types. Given $S <: T$ and a type constructor $F$:
- Covariant: $ F \langle S \rangle <: F \langle T \rangle $ (preserves direction)
- Contravariant: $ F \langle S \rangle :> F \langle T \rangle $ (reverses direction)
- Invariant: No subtyping relationship is guaranteed between $F\langle S \rangle$ and $F \langle T \rangle$.
def vector_add(
a: TileTensor[float_dtype, type_of(layout), element_size=1, ...],
b: TileTensor[float_dtype, type_of(layout), element_size=1, ...],
result: TileTensor[
mut=True, float_dtype, type_of(layout), element_size=1, ...
],
):
var i = global_idx.x
if i < layout.size():
result[i] = a[i] + b[i]
# Generic struct equality using compile-time reflection.
def __eq__(self, other: Self) -> Bool:
comptime r = reflect[Self]()
comptime for i in range(r.field_names().size):
comptime assert conforms_to(r.field_types()[i], Equatable)
if r.field_ref[i](self) != r.field_ref[i](other):
return False
return True
In languages like Java or C++, subtyping is driven by inheritance; if Cat extends Animal, then Cat is a subtype of Animal.
Rust instead has zero inheritance-based subtyping for user-defined types (like structs or enums). The only subtyping in Rust exists over lifetimes, higher-ranked trait bounds (HRTB), and supertraits.
Longer lifetime is a subtype of a shorter lifetime.
This is natural considering Liskov substitution principle; a reference that is valid for a longer duration (&'a T) can safely be used anywhere that expects a shorter duration (&'b T). The longer lifetime satisfies all constraints of the shorter one.
// In relationship where 'a outlives 'b ('a: 'b, meaning 'a is a subtype of 'b),
// &'a T can be safely passed anywhere that expects &'b T.
fn main()
{
let x = 10; // ---------+ 'a (longer lifetime)
{ // |
let y = 20; // ---+ | 'b (shorter lifetime)
// | |
let ref_b: &'b i32 = &x; // | <- &'a i32 is safely downcasted to &'b i32
// | |
} // ---+ |
} // ---------+
Supertraits
In the context of trait objects, dyn Sub is a subtype of dyn Super.
Structural and Inferred Variance
- Covariant:
&'a T,Box<T>,Rc<T>,Vec<T>,dyn Sub - Contravariant:
Fn(T) - Invariant:
&mut T,Unsafecell<T>,Cell<T>
Note that &'a mut T is covariant over 'a and strictly invariant over T, while &'a T is covariant over both 'a and T.
// The actual data exists at the 'static level.
let mut static_data: &'static str = "I live forever";
{ // 'a block
// short-living str (&'a str)
let short_data = String::from("short");
let proxy: &mut &/*'a*/str = &mut static_data;
// IF this compiles, `static_data` variable would now point to the memory address of `short_data`,
// which has a much shorter lifetime than 'static!
*proxy = &short_data;
}
// `static_data` is still alive, but the data (address) it holds is already dead.
println!("{}", static_data); // Use-after-free!
error[E0597]: `short_data` does not live long enough
--> src/main.rs:6:18
|
2 | let mut static_data: &'static str = "hi";
| ------------ type annotation requires that `short_data` is borrowed for `'static`
3 | {
4 | let short_data = String::from("short");
| ---------- binding `short_data` declared here
5 | let proxy: &mut &/*'a*/str = &mut static_data;
6 | *proxy = &short_data;
| ^^^^^^^^^^^ borrowed value does not live long enough
7 | }
| - `short_data` dropped here while still borrowed
For more information about this error, try `rustc --explain E0597`.