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`.