PhantomData for Lifetime Markers
Tutorial Video
Text description (accessibility)
This video demonstrates the "PhantomData for Lifetime Markers" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Sometimes a struct logically borrows from an external lifetime but does not store any reference — perhaps it holds a raw pointer, a numeric ID, or an opaque handle. Key difference from OCaml: 1. **Runtime cost**: `PhantomData` is zero
Tutorial
The Problem
Sometimes a struct logically borrows from an external lifetime but does not store any reference — perhaps it holds a raw pointer, a numeric ID, or an opaque handle. Without PhantomData, the compiler has no way to know the struct's relationship to that lifetime, leading to incorrect variance and missing lifetime checks. PhantomData<&'a T> is a zero-size type that carries lifetime and variance information without storing any data. It is essential for safe wrappers around raw pointers, arena allocators, typed indices, and foreign-function handles.
🎯 Learning Outcomes
PhantomData<&'a T> is needed when a struct logically borrows 'a but has no reference fieldHandle<'a, T> with PhantomData<&'a T> prevents handles from outliving their sourceIndex<T> uses PhantomData<T> for type-safety without storing a TPhantomData affects variance (covariant, contravariant, invariant)PhantomData appears: arena allocators, foreign-function handles, typed indices, raw pointer wrappersCode Example
#![allow(clippy::all)]
//! PhantomData for Lifetime Markers
//!
//! Using PhantomData to carry lifetime information.
use std::marker::PhantomData;
/// Struct that conceptually borrows from 'a.
pub struct Handle<'a, T> {
id: usize,
_marker: PhantomData<&'a T>,
}
impl<'a, T> Handle<'a, T> {
pub fn new(id: usize) -> Self {
Handle {
id,
_marker: PhantomData,
}
}
pub fn id(&self) -> usize {
self.id
}
}
/// Typed index into a collection.
pub struct Index<T> {
idx: usize,
_marker: PhantomData<T>,
}
impl<T> Index<T> {
pub fn new(idx: usize) -> Self {
Index {
idx,
_marker: PhantomData,
}
}
pub fn get(self) -> usize {
self.idx
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_handle() {
let h: Handle<i32> = Handle::new(42);
assert_eq!(h.id(), 42);
}
#[test]
fn test_index() {
let idx: Index<String> = Index::new(5);
assert_eq!(idx.get(), 5);
}
}Key Differences
PhantomData is zero-size — no runtime overhead; OCaml phantom types are also zero-cost at runtime since the 'a type parameter is erased.PhantomData<&'a T> enforces that Handle<'a, T> cannot outlive 'a at compile time; OCaml phantom types cannot express lifetime constraints.PhantomData precisely controls variance (covariant, contravariant, invariant); OCaml phantom types are covariant by default unless annotated.PhantomData<T> to tell the compiler what the pointer "logically owns"; OCaml raw pointers (via Bigarray or ctypes) rely on programmer discipline.OCaml Approach
OCaml achieves typed index safety through phantom types using abstract module signatures or the Ppx_phantom approach:
type 'a index = Index of int
let make_index n : 'a index = Index n
let get (Index n) = n
(* User_index and Post_index are the same type at runtime but distinct by convention *)
OCaml's phantom types are a convention — the runtime has no distinction. Rust's PhantomData enforces the distinction at the type level.
Full Source
#![allow(clippy::all)]
//! PhantomData for Lifetime Markers
//!
//! Using PhantomData to carry lifetime information.
use std::marker::PhantomData;
/// Struct that conceptually borrows from 'a.
pub struct Handle<'a, T> {
id: usize,
_marker: PhantomData<&'a T>,
}
impl<'a, T> Handle<'a, T> {
pub fn new(id: usize) -> Self {
Handle {
id,
_marker: PhantomData,
}
}
pub fn id(&self) -> usize {
self.id
}
}
/// Typed index into a collection.
pub struct Index<T> {
idx: usize,
_marker: PhantomData<T>,
}
impl<T> Index<T> {
pub fn new(idx: usize) -> Self {
Index {
idx,
_marker: PhantomData,
}
}
pub fn get(self) -> usize {
self.idx
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_handle() {
let h: Handle<i32> = Handle::new(42);
assert_eq!(h.id(), 42);
}
#[test]
fn test_index() {
let idx: Index<String> = Index::new(5);
assert_eq!(idx.get(), 5);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_handle() {
let h: Handle<i32> = Handle::new(42);
assert_eq!(h.id(), 42);
}
#[test]
fn test_index() {
let idx: Index<String> = Index::new(5);
assert_eq!(idx.get(), 5);
}
}
Deep Comparison
OCaml vs Rust: lifetime phantom
See example.rs and example.ml for implementations.
Key Differences
Exercises
struct GenerationHandle<'arena, T> { id: u32, _p: PhantomData<&'arena T> } where 'arena ensures the handle cannot outlive the arena it was allocated from.struct Matrix<T, Rows, Cols> { data: Vec<T>, _p: PhantomData<(Rows, Cols)> } and implement fn transpose(m: Matrix<T, R, C>) -> Matrix<T, C, R> — use phantom row/col types to prevent transposing the wrong way.Handle<'a, T> to use PhantomData<&'a mut T> — explain what changes in terms of variance and what programs the compiler now rejects.