Variance: Covariant, Contravariant, Invariant
Tutorial Video
Text description (accessibility)
This video demonstrates the "Variance: Covariant, Contravariant, Invariant" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Variance describes how subtyping relationships on types propagate through type constructors. Key difference from OCaml: 1. **Automatic inference**: Rust infers variance from how parameters are used (in position, out position, both); OCaml requires explicit `+`/`
Tutorial
The Problem
Variance describes how subtyping relationships on types propagate through type constructors. In Rust's lifetime system: if 'long: 'short (long outlives short), does Container<'long> also satisfy Container<'short> requirements? The answer depends on whether Container is covariant, contravariant, or invariant in its lifetime. Getting variance wrong leads to subtle unsoundness bugs — particularly with mutable references, which must be invariant. Rust computes variance automatically based on how lifetime parameters are used, and PhantomData lets you declare variance for types that need it explicitly.
🎯 Learning Outcomes
&'a T is covariant (longer lifetime usable as shorter)&'a mut T is invariant (cannot substitute a different lifetime without unsoundness)PhantomData<&'a T> makes a wrapper covariant in 'aPhantomData<&'a mut T> makes a wrapper invariant in 'aCode Example
// Covariant: &'a T, Box<T>, Vec<T>
// Contravariant: fn(T) -> ()
// Invariant: &'a mut T, Cell<T>
// 'static can be used as shorter lifetime
fn demo<'short>(s: &'static str) -> &'short str { s }Key Differences
+/- annotations or infers from usage.'a directly; OCaml has no lifetimes, so variance only applies to type parameters.&mut T prevents a class of memory unsoundness bugs that would be possible with covariant mutable references; OCaml's GC eliminates the corresponding risks.PhantomData to declare variance for types that don't directly store T (e.g., raw pointer wrappers); OCaml achieves the same through direct type parameter annotation.OCaml Approach
OCaml's type system has variance annotations on type parameters (+'a for covariant, -'a for contravariant, no annotation for invariant). However, these apply to type parameters, not lifetimes, since OCaml has no lifetime system:
type +'a covariant = Cov of 'a (* covariant in 'a *)
type -'a contravariant = Contra of ('a -> unit) (* contravariant *)
Full Source
#![allow(clippy::all)]
//! Variance: Covariant, Contravariant, Invariant
//!
//! How subtyping propagates through type constructors.
use std::marker::PhantomData;
/// Covariant wrapper (like &'a T).
pub struct Covariant<'a, T> {
_marker: PhantomData<&'a T>,
}
/// Invariant wrapper (like &'a mut T).
pub struct Invariant<'a, T> {
_marker: PhantomData<&'a mut T>,
}
/// Covariant: longer lifetime can be used where shorter expected.
pub fn covariant_demo<'short>(s: &'short str) -> &'short str {
let long: &'static str = "static";
// 'static coerces to 'short — covariant
long
}
/// Demonstrate covariance with Vec.
pub fn vec_covariance<'a>(v: Vec<&'static str>) -> Vec<&'a str> {
// Vec<&'static T> can coerce to Vec<&'a T> for immutable use
v
}
/// Cell<T> is invariant in T.
pub fn invariant_example() {
use std::cell::Cell;
let cell: Cell<i32> = Cell::new(5);
cell.set(10);
assert_eq!(cell.get(), 10);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_covariant_demo() {
let local = String::from("local");
let result = covariant_demo(&local);
assert_eq!(result, "static");
}
#[test]
fn test_vec_covariance() {
let v: Vec<&'static str> = vec!["a", "b", "c"];
let v2: Vec<&str> = vec_covariance(v);
assert_eq!(v2.len(), 3);
}
#[test]
fn test_invariant() {
invariant_example();
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_covariant_demo() {
let local = String::from("local");
let result = covariant_demo(&local);
assert_eq!(result, "static");
}
#[test]
fn test_vec_covariance() {
let v: Vec<&'static str> = vec!["a", "b", "c"];
let v2: Vec<&str> = vec_covariance(v);
assert_eq!(v2.len(), 3);
}
#[test]
fn test_invariant() {
invariant_example();
}
}
Deep Comparison
OCaml vs Rust: Variance
OCaml
(* Variance annotations in type definitions *)
type +'a covariant = 'a list
type -'a contravariant = 'a -> unit
type 'a invariant = { mutable value: 'a }
Rust
// Covariant: &'a T, Box<T>, Vec<T>
// Contravariant: fn(T) -> ()
// Invariant: &'a mut T, Cell<T>
// 'static can be used as shorter lifetime
fn demo<'short>(s: &'static str) -> &'short str { s }
Key Differences
Exercises
struct ReadOnly<'a, T>(PhantomData<&'a T>) and write a function demonstrating a ReadOnly<'static, str> can be used where ReadOnly<'short, str> is expected.struct Mutable<'a, T>(PhantomData<&'a mut T>) and verify in a comment that substituting 'long for 'short would be rejected by the compiler.PhantomData<fn(T) -> ()> for contravariance and implement a struct Sink<T>(PhantomData<fn(T)>) that is contravariant in T.