Lifetime Coercion and Subtyping
Tutorial Video
Text description (accessibility)
This video demonstrates the "Lifetime Coercion and Subtyping" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Lifetime subtyping is the mechanism that makes Rust's borrow checker flexible without requiring programmers to always use exactly matching lifetimes. Key difference from OCaml: 1. **Automatic coercion**: Rust automatically coerces `'long` to `'short` at assignment; OCaml has no lifetime coercion because lifetimes do not exist.
Tutorial
The Problem
Lifetime subtyping is the mechanism that makes Rust's borrow checker flexible without requiring programmers to always use exactly matching lifetimes. The rule is simple: a longer lifetime can always be used where a shorter one is expected, because a reference valid for longer is certainly valid for shorter. This is similar to how a subtype can be used where its supertype is expected. Without this coercion, every reference would require perfectly matched lifetime scopes, making APIs rigid and hard to use.
🎯 Learning Outcomes
'static references can be passed to functions expecting shorter-lived references'long: 'short (outlives) expresses that 'long is a subtype of 'short'static items in Vec<&'short str>demonstrate_variance<'long: 'short, 'short> expresses the outlives constraintCode Example
// Longer lifetime coerces to shorter
pub fn use_briefly<'short>(s: &'short str) -> usize {
s.len()
}
// 'static (longer) can be used where 'short expected
fn demo() {
let static_str: &'static str = "forever";
use_briefly(static_str); // 'static -> 'short coercion
}
// Explicit bound: 'long outlives 'short
fn variance<'long: 'short, 'short>(r: &'long str) -> &'short str {
r // covariant: longer to shorter
}Key Differences
'long to 'short at assignment; OCaml has no lifetime coercion because lifetimes do not exist.'long: 'short syntax expresses a compile-time constraint; OCaml programs never write or verify such relationships.OCaml Approach
OCaml has no lifetime coercion because there are no lifetime annotations. The GC ensures all referenced values are kept alive. Subtyping in OCaml is structural (via polymorphic variants and object types), not lifetime-based.
(* No equivalent concept — all references are GC-managed *)
let use_briefly s = String.length s
let _ = use_briefly "static string" (* always fine *)
Full Source
#![allow(clippy::all)]
//! Lifetime Coercion and Subtyping
//!
//! Longer lifetimes can be used where shorter ones are required.
/// Accepts a reference valid for at least 'short duration.
pub fn use_briefly<'short>(s: &'short str) -> usize {
s.len()
}
/// Demonstrate implicit coercion: longer lifetime used as shorter.
pub fn coercion_demo() -> usize {
// 'static is longer than any local lifetime
let static_str: &'static str = "I live forever";
// 'static coerces to 'short when passed to use_briefly
use_briefly(static_str)
}
/// Store in a Vec with shorter lifetime requirement.
pub fn store_with_coercion<'short>(storage: &mut Vec<&'short str>, item: &'static str) {
// 'static can be stored where 'short is expected
storage.push(item);
}
/// Function demonstrating variance.
pub fn demonstrate_variance<'long: 'short, 'short>(
long_ref: &'long str,
_short_ref: &'short str,
) -> &'short str {
// 'long can be used as 'short (covariance)
long_ref
}
/// Reborrowing: creating a shorter borrow from a longer one.
pub fn reborrow_demo() {
let owned = String::from("hello");
let long_borrow: &str = &owned;
// Reborrow with shorter lifetime
let short_borrow: &str = long_borrow;
assert_eq!(short_borrow, "hello");
}
/// Reference to reference coercion.
pub fn ref_ref_coercion<'a, 'b: 'a>(r: &'a &'b str) -> &'a str {
*r
}
/// Struct that accepts shorter lifetime.
pub struct Holder<'a> {
pub data: &'a str,
}
impl<'a> Holder<'a> {
/// Can accept 'static (longer) for 'a.
pub fn from_static(s: &'static str) -> Holder<'static> {
Holder { data: s }
}
/// General constructor.
pub fn new(data: &'a str) -> Self {
Holder { data }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_static_to_short() {
let result = coercion_demo();
assert_eq!(result, 14); // "I live forever".len()
}
#[test]
fn test_store_with_coercion() {
let owned = String::from("local");
let mut storage: Vec<&str> = vec![&owned];
store_with_coercion(&mut storage, "static");
assert_eq!(storage.len(), 2);
}
#[test]
fn test_demonstrate_variance() {
let long = String::from("long lived");
{
let short = String::from("short");
let result = demonstrate_variance(&long, &short);
assert_eq!(result, "long lived");
}
}
#[test]
fn test_reborrow() {
let owned = String::from("test");
let r1: &str = &owned;
let r2: &str = r1; // reborrow
assert_eq!(r1, r2);
}
#[test]
fn test_ref_ref() {
let s = "hello";
let r: &str = &s;
let result = ref_ref_coercion(&r);
assert_eq!(result, "hello");
}
#[test]
fn test_holder_from_static() {
let holder = Holder::from_static("static string");
assert_eq!(holder.data, "static string");
}
#[test]
fn test_holder_from_local() {
let local = String::from("local");
let holder = Holder::new(&local);
assert_eq!(holder.data, "local");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_static_to_short() {
let result = coercion_demo();
assert_eq!(result, 14); // "I live forever".len()
}
#[test]
fn test_store_with_coercion() {
let owned = String::from("local");
let mut storage: Vec<&str> = vec![&owned];
store_with_coercion(&mut storage, "static");
assert_eq!(storage.len(), 2);
}
#[test]
fn test_demonstrate_variance() {
let long = String::from("long lived");
{
let short = String::from("short");
let result = demonstrate_variance(&long, &short);
assert_eq!(result, "long lived");
}
}
#[test]
fn test_reborrow() {
let owned = String::from("test");
let r1: &str = &owned;
let r2: &str = r1; // reborrow
assert_eq!(r1, r2);
}
#[test]
fn test_ref_ref() {
let s = "hello";
let r: &str = &s;
let result = ref_ref_coercion(&r);
assert_eq!(result, "hello");
}
#[test]
fn test_holder_from_static() {
let holder = Holder::from_static("static string");
assert_eq!(holder.data, "static string");
}
#[test]
fn test_holder_from_local() {
let local = String::from("local");
let holder = Holder::new(&local);
assert_eq!(holder.data, "local");
}
}
Deep Comparison
OCaml vs Rust: Lifetime Coercion
OCaml
(* No lifetime coercion concept — GC manages all *)
let use_briefly s = String.length s
let demo () =
let long_lived = "static string" in
use_briefly long_lived
Rust
// Longer lifetime coerces to shorter
pub fn use_briefly<'short>(s: &'short str) -> usize {
s.len()
}
// 'static (longer) can be used where 'short expected
fn demo() {
let static_str: &'static str = "forever";
use_briefly(static_str); // 'static -> 'short coercion
}
// Explicit bound: 'long outlives 'short
fn variance<'long: 'short, 'short>(r: &'long str) -> &'short str {
r // covariant: longer to shorter
}
Key Differences
Exercises
fn most_general<'a>(s: &'a str) -> &'a str { s } and show it can accept both &'static str and short-lived references.Vec<&str> and insert both &'static str literals and references to local String values — verify the compiler correctly constrains the vec's lifetime.'a: 'b: 'c where the first returns &'a str, the second accepts &'b str, and the third accepts &'c str — show the chain compiles.