๐ŸŽ“ A1Learner
๐Ÿ”

Rust Variance

Variance is the rule for how subtyping passes through a type.

Covariant: the shrink passes through a Vec

total_len accepts a Vec of references with any one lifetime. A Vec of &'static str qualifies, because Vec is covariant over the type it holds:

fn total_len(words: Vec<&str>) -> usize {
    words.iter().map(|w| w.len()).sum()
}
let fixed: Vec<&'static str> = vec!["rust", "is", "fun"];
println!("total = {}", total_len(fixed));

Invariant: the &mut store is refused

store writes a value through a &mut. If the shrink passed through &mut here, hello would end up pointing at world. Yet hello is promised for the whole program. And world dies at the end of main. The wall stops exactly that:

fn store<T>(slot: &mut T, value: T) { *slot = value; }
let mut hello: &'static str = "hello";
let world = String::from("world");
store(&mut hello, &world); // error E0597: `world` does not live long enough
println!("{}", hello);

Invariant: Cell follows the same rule

A Cell lets you change a value through a shared reference. It gives the same writing power as &mut. You reach it from the other side. It gets the same wall:

use std::cell::Cell;
let cell: Cell<&'static str> = Cell::new("hello");
let world = String::from("world");
cell.set(&world); // error E0597: `world` does not live long enough

Contravariant: function arguments flip

shout accepts a reference of any lifetime. That wider promise makes it acceptable where a function accepting only &'static str is required. Flips like this happen only in argument position:

fn shout(s: &str) { println!("{}", s.to_uppercase()); }
let f: fn(&'static str) = shout;
f("static text");

Example

fn total_len(words: Vec<&str>) -> usize {
    words.iter().map(|w| w.len()).sum()
}
fn main() {
    let fixed: Vec<&'static str> = vec!["rust", "is", "fun"];
    println!("total = {}", total_len(fixed));
    let eternal: &'static str = "eternal";
    let brief: &str = eternal;
    println!("brief = {} ({} letters)", brief, brief.len());
}

Key points

  • Variance: the rule for how subtyping in a parameter affects the whole type.
  • It says whether F<Sub> may stand in for F<Super>.
  • Covariant: the relationship passes through.
  • &'a T, Box<T> and Vec<T> are covariant, so a Vec<&'static str> may serve as a Vec<&'a str>
  • Invariant: no subtyping at all.
  • &'a mut T and Cell<T> demand the exact type they were given
  • Contravariant: the relationship flips.
  • Only function arguments behave this way.
  • A fn that accepts any &str may serve where a fn accepting only &'static str is needed.
  • Invariance is a safety wall.
  • Through a &mut or a Cell you can write a new value in.
  • A shrink there would let a short-lived value slip into a long-lived slot.
  • A struct inherits the variance of its fields.
  • A field of &T makes it covariant.
  • One Cell<T> field makes the whole struct invariant over T.
  • Invariance wins every conflict.

In this example, the shrink travels two covariant paths. total_len wants a Vec of references under one lifetime. fixed holds &'static str values. Vec is covariant, so the whole Vec shrinks to fit. It prints total = 9 for 4 + 2 + 3 letters. Below, the same shrink happens with no container at all. eternal is &'static str. brief asks for a shorter &str. The assignment prints brief = eternal (7 letters). Both paths are read-only. Nothing can write a shorter-lived value back through them. That is exactly why covariance is safe here.

Note

You never write a variance anywhere.

Rust works it out from the fields.

You meet variance only when you read an error message.

Then you ask why the shrink was refused.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

Why is &'a mut T invariant over T?