whexy1999

Variance in Rust (Rust 中的协变、逆变与不变)

Whexy /
February 21, 2021

I’m watching Jon Gjengset’s live coding stream. And the topic is “Subtyping and Variance”. This is my note.

Jon Gjengset’s live coding stream

Lifetime shrinking

The Rust compiler will automatically shrink the lifetime of the parameter to the shortest one. For example,

fn main() {
	let s = String::new();
	let x = "static str"; // `x` is `&'static str`
	let mut y = &*s; // `y` is `&'s str`
	y = x;
	// Still compilable!
  // Rust automatically shrink the lifetime static to s.
}

That makes sense, because you can always trust a value from who lives longer, without concerning about the value somehow goes invalid. What’s behind the scene is that Rust have a system of subtyping and variance.

Subtype

Just think of an example in Java, class Cat is a subtype of the class Animal. In brief, we say T is a subtype of U (notation T <: U) when T is at least as useful as U. T can do anything that U can do, but T may have the ability of other things. In Rust, the lifetime ’static is a subtype of every lifetime. Rust compiler then uses different variance rules to check whether the program should be compiled or not.

Variance

You may understand variance in many other programming languages. And there are three types of variance in programming, called covariance, contra-variance, and invariance.

Covariance

Covariance is the most common case. Most things in Rust is covariance. For example,

/// define a function which takes an lifetime sticker `a`
fn foo(_: &'a str) {}

// and you can call the function with
foo(&'a str);
// or
foo(&'static str);

In the example, we can give the function with parameter whose lifetime is no matter a or static. That is because static is a subtype of a. The static str lives longer than the required ’a, so there should be no concern that the borrowed variable would be unexpectedly dropped.

Contra-variance

Let’s consider the high-rank function example below.

/// define a function which takes a function,
/// which takes a lifetime sticker `a`.
fn foo(bar: Fn(&'a str) -> ()) {
	bar(str);
}

let x : Fn(&'a str) -> ();
foo(x); // that makes sense.

let y : Fn(&'static str) -> ();
foo(y); // should that make sense ???

Should foo(y) be compilable? Definitely not! Let’s say if foo(y) compiles, then we are actually doing things in the high-rank function like:

let baz = &*String::new();
// lifetime of baz is shorter than 'static
fn y(_: &'static str) {}
// an function that needs a static borrowing
y(baz);
// [!!] Should not compile
// because a static lifetime is required.

The caller gives a parameter with limited lifetime. But the function we get requires a static lifetime parameter. That cannot be allowed. However, let’s consider another example:

/// define a function which takes a function,
/// which takes a parameter with static lifetime.
fn foo(bar: Fn(&'static str) -> ()) {
	bar("Hello Whexy~");
}

let x : Fn(&'static str) -> ();
foo(x); // that makes sense.

let y : Fn(&'a str) -> ();
foo(y); // that makes sense too.

This example is perfect compiled. Because the function y requires a parameter with limited lifetime. The caller gives it a static parameter which lives longer. Again, there should be no concern that the borrowed variable would be unexpectedly dropped. So the contra-variance is a specific rule. T <: U ==> Fn(U) <: Fn(T)

Invariance

Invariance means “no variance”. In a short word, “just pass me exact the thing I require, no tricks.” Let’s see this example:

fn foo(s: &mut &'a str, x: &'a str) {
	*s = x;
}
let mut x = "Hello"; // x : &'static str
let z = String::new();
foo(x, &z); // foo(&'static str, &'z str)
drop(z);
println!("{}", x); // OOPS!

The code cannot compile, because we are going to access x, which points to a dropped memory area. In fact, in &'a mut T, T is invariance. However, the 'a is covariance. It’s not hard to figure out, so I’m left this to you as an exercise.

Reference: variance of types

Variance of types are listed in “The Rustonomicon”

© LICENSED UNDER CC BY-NC-SA 4.0