738-phantom-type-basics — Phantom Type Basics
Tutorial Video
Text description (accessibility)
This video demonstrates the "738-phantom-type-basics — Phantom Type Basics" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Sometimes you need a type to carry extra compile-time information that has no runtime representation. Key difference from OCaml: 1. **Syntax**: Rust requires `PhantomData<Tag>` as an explicit field; OCaml's phantom variables appear naturally in type signatures without a dummy field.
Tutorial
The Problem
Sometimes you need a type to carry extra compile-time information that has no runtime representation. A UserId and a ProductId are both u64, but mixing them up is a logic error. Phantom types solve this: Tagged<u64, UserTag> and Tagged<u64, ProductTag> are different types at compile time but identical at runtime. This pattern prevents unit confusion (meters vs. feet), validates data provenance (raw vs. validated input), and creates marker-based permission systems — all at zero runtime cost.
🎯 Learning Outcomes
PhantomData<Tag> to carry type-level information without runtime overheadTagged<T, Tag> wrapper that makes distinct "branded" types from the same value typeValidated and Unvalidated markers on UserIdPhantomData is necessary for the compiler to accept unused type parametersCode Example
#![allow(clippy::all)]
//! # Phantom Type Basics
//! PhantomData for type-level information
use std::marker::PhantomData;
/// Wrapper with phantom type parameter
pub struct Tagged<T, Tag> {
pub value: T,
_tag: PhantomData<Tag>,
}
impl<T, Tag> Tagged<T, Tag> {
pub fn new(value: T) -> Self {
Tagged {
value,
_tag: PhantomData,
}
}
pub fn into_inner(self) -> T {
self.value
}
}
/// Type-level markers
pub struct Validated;
pub struct Unvalidated;
/// ID that tracks validation status
pub struct UserId<State>(u64, PhantomData<State>);
impl UserId<Unvalidated> {
pub fn new(id: u64) -> Self {
UserId(id, PhantomData)
}
pub fn validate(self) -> Option<UserId<Validated>> {
if self.0 > 0 {
Some(UserId(self.0, PhantomData))
} else {
None
}
}
}
impl UserId<Validated> {
pub fn get(&self) -> u64 {
self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tagged() {
struct MyTag;
let t: Tagged<i32, MyTag> = Tagged::new(42);
assert_eq!(t.into_inner(), 42);
}
#[test]
fn test_validated() {
let id = UserId::new(123);
let validated = id.validate().unwrap();
assert_eq!(validated.get(), 123);
}
}Key Differences
PhantomData<Tag> as an explicit field; OCaml's phantom variables appear naturally in type signatures without a dummy field.PhantomData<(A, B)>; OCaml uses multiple type variables directly.PhantomData<T> is zero bytes; OCaml's phantom variables add no runtime overhead.OCaml Approach
OCaml phantom types use type variables in a similar position: type ('state) user_id = UserId of int64. Modules provide encapsulation: only the module that implements validate can construct Validated user_id. OCaml 5 adds [@@unboxed] to eliminate even the boxing overhead. Jane Street's Id module uses this exact pattern for all entity IDs in their trading systems.
Full Source
#![allow(clippy::all)]
//! # Phantom Type Basics
//! PhantomData for type-level information
use std::marker::PhantomData;
/// Wrapper with phantom type parameter
pub struct Tagged<T, Tag> {
pub value: T,
_tag: PhantomData<Tag>,
}
impl<T, Tag> Tagged<T, Tag> {
pub fn new(value: T) -> Self {
Tagged {
value,
_tag: PhantomData,
}
}
pub fn into_inner(self) -> T {
self.value
}
}
/// Type-level markers
pub struct Validated;
pub struct Unvalidated;
/// ID that tracks validation status
pub struct UserId<State>(u64, PhantomData<State>);
impl UserId<Unvalidated> {
pub fn new(id: u64) -> Self {
UserId(id, PhantomData)
}
pub fn validate(self) -> Option<UserId<Validated>> {
if self.0 > 0 {
Some(UserId(self.0, PhantomData))
} else {
None
}
}
}
impl UserId<Validated> {
pub fn get(&self) -> u64 {
self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tagged() {
struct MyTag;
let t: Tagged<i32, MyTag> = Tagged::new(42);
assert_eq!(t.into_inner(), 42);
}
#[test]
fn test_validated() {
let id = UserId::new(123);
let validated = id.validate().unwrap();
assert_eq!(validated.get(), 123);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tagged() {
struct MyTag;
let t: Tagged<i32, MyTag> = Tagged::new(42);
assert_eq!(t.into_inner(), 42);
}
#[test]
fn test_validated() {
let id = UserId::new(123);
let validated = id.validate().unwrap();
assert_eq!(validated.get(), 123);
}
}
Deep Comparison
Phantom Type Basics
See example files for comparison.
Exercises
ProductId<State> and OrderId<State> phantom types and write a function create_order(user: UserId<Validated>, product: ProductId<Validated>) -> OrderId<Unvalidated>.Sanitized marker and a sanitize(raw: Tagged<String, Raw>) -> Tagged<String, Sanitized> function that strips HTML tags. Ensure render only accepts Tagged<String, Sanitized>.TypeMap<Tag, V> that stores values keyed by phantom-tagged keys, preventing retrieval with the wrong tag type.