๐Ÿฆ€ Functional Rust
๐ŸŽฌ Rust Ownership in 30 seconds Visual walkthrough of ownership, moves, and automatic memory management.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Each value in Rust has exactly one owner โ€” when the owner goes out of scope, the value is dropped

โ€ข Assignment moves ownership by default; the original binding becomes invalid

โ€ข Borrowing (&T / &mut T) lets you reference data without taking ownership

โ€ข The compiler enforces: many shared references OR one mutable reference, never both

โ€ข No garbage collector needed โ€” memory is freed deterministically at scope exit

738: PhantomData: Phantom Types and Type Markers

Difficulty: 4 Level: Expert `PhantomData<T>` is zero bytes at runtime but makes the compiler treat a struct as if it owns or uses `T` โ€” enabling typed IDs, capability tokens, and variance annotations with zero overhead.

The Problem This Solves

Stringly-typed systems are fragile. A function `fn get_user(id: u64)` accepts any `u64` โ€” you could accidentally pass an `order_id` and the compiler won't object. Runtime bugs from confusing IDs of different entity types are common in large codebases and expensive to track down. The brute-force fix is newtype wrappers: `struct UserId(u64)` and `struct OrderId(u64)`. They're different types, but you still need to write a dozen `From`/`Into` impls if you want them to share methods. If you want `Id<User>` and `Id<Order>` to share the same generic `Id` implementation while remaining distinct types, you need `PhantomData`. More broadly, `PhantomData` is the tool for encoding type-level information in a struct when that information has no runtime representation. Permission tokens, variance markers, lifetime attachments โ€” all use `PhantomData` as the carrier.

The Intuition

`PhantomData<T>` is a zero-sized type that tells the compiler "this struct logically involves `T`, even though no field actually holds a `T`." The compiler needs this for two reasons: variance analysis (how lifetimes flow through the type) and drop-check analysis (whether dropping the struct might drop a `T`). For user code, the most common use is type-level tagging: `Id<User>` and `Id<Order>` are the same struct with the same `u64` field, but they're different types because their `Entity` type parameter differs. Assigning one to the other is a compile error. At runtime, both are just a `u64` โ€” `PhantomData` adds exactly zero bytes.

How It Works in Rust

use std::marker::PhantomData;

// Entity markers โ€” zero-sized, exist only for type checking
pub struct User;
pub struct Order;

// Generic ID โ€” same struct, different types depending on Entity
pub struct Id<Entity> {
 value:   u64,
 _entity: PhantomData<Entity>,  // 0 bytes, makes Id<User> โ‰  Id<Order>
}

impl<Entity> Id<Entity> {
 pub fn new(value: u64) -> Self { Id { value, _entity: PhantomData } }
 pub fn value(&self) -> u64 { self.value }
}

pub type UserId  = Id<User>;
pub type OrderId = Id<Order>;

let user_id:  UserId  = Id::new(42);
let order_id: OrderId = Id::new(42);

// Same inner value, different types โ€” compiler rejects mixing:
// let wrong: UserId = order_id;  // ERROR: expected Id<User>, got Id<Order>

// Size: just the u64 โ€” PhantomData adds nothing
assert_eq!(std::mem::size_of::<UserId>(), std::mem::size_of::<u64>());  // 8 bytes

// โ”€โ”€ Capability tokens โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
pub struct ReadPerm;
pub struct WritePerm;

pub struct Token<Perm> {
 id:    u32,
 _perm: PhantomData<Perm>,
}

// Functions only accept the right token type
fn read_resource(tok: &Token<ReadPerm>, name: &str) -> String { /* ... */ }
fn write_resource(tok: &Token<WritePerm>, name: &str, data: &str) -> String { /* ... */ }

let rt: Token<ReadPerm>  = Token::new(1);
let wt: Token<WritePerm> = Token::new(2);

read_resource(&rt, "config");        // fine
write_resource(&wt, "config", "v"); // fine

// read_resource(&wt, "x");          // ERROR: expected Token<ReadPerm>, got Token<WritePerm>
// write_resource(&rt, "x", "y");    // ERROR: same

// โ”€โ”€ Key facts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
assert_eq!(std::mem::size_of::<PhantomData<String>>(), 0);  // always zero
assert_eq!(std::mem::size_of::<PhantomData<Vec<u8>>>(), 0); // always zero

What This Unlocks

Key Differences

ConceptOCamlRust
Phantom types`type 'a t = T` โ€” `'a` is phantom if unused in `T``struct Foo<T> { _t: PhantomData<T> }` โ€” explicit phantom field
Zero-cost type tagPhantom type variable (erased at runtime)`PhantomData<T>` โ€” zero bytes, erased at runtime
Typed IDs`type user_id = int` โ€” same underlying type, aliased`Id<User>` vs `Id<Order>` โ€” structurally different types; mixing is error
Capability tokensPhantom type in module signature`Token<ReadPerm>` vs `Token<WritePerm>` โ€” separate generic instantiations
/// 738: PhantomData โ€” phantom types and type markers
/// PhantomData<T> is zero bytes at runtime but carries T for type checking.

use std::marker::PhantomData;

// โ”€โ”€ Basic phantom type โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// A typed ID: wraps a `u64` but is tagged with a phantom `Entity` type.
/// `UserId` and `OrderId` are different types even though both hold a `u64`.
pub struct Id<Entity> {
    value:   u64,
    _entity: PhantomData<Entity>,
}

impl<Entity> Id<Entity> {
    pub fn new(value: u64) -> Self {
        Id { value, _entity: PhantomData }
    }
    pub fn value(&self) -> u64 { self.value }
}

impl<Entity> std::fmt::Debug for Id<Entity> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Id({})", self.value)
    }
}

// Entity marker types
pub struct User;
pub struct Order;

pub type UserId  = Id<User>;
pub type OrderId = Id<Order>;

// โ”€โ”€ Capability tokens โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

pub struct ReadPerm;
pub struct WritePerm;

pub struct Token<Perm> {
    id:    u32,
    _perm: PhantomData<Perm>,
}

impl<Perm> Token<Perm> {
    pub fn new(id: u32) -> Self { Token { id, _perm: PhantomData } }
    pub fn id(&self) -> u32 { self.id }
}

fn read_resource(_tok: &Token<ReadPerm>, name: &str) -> String {
    format!("Reading '{}' with token {}", name, _tok.id())
}

fn write_resource(_tok: &Token<WritePerm>, name: &str, data: &str) -> String {
    format!("Writing '{}' = '{}' with token {}", name, data, _tok.id())
}

// โ”€โ”€ Size verification โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

fn main() {
    // Typed IDs
    let user_id:  UserId  = Id::new(42);
    let order_id: OrderId = Id::new(42);

    println!("UserId:  {:?}", user_id);
    println!("OrderId: {:?}", order_id);
    println!("Size of Id<User>:   {} bytes", std::mem::size_of::<UserId>());
    println!("Size of Id<Order>:  {} bytes", std::mem::size_of::<OrderId>());

    // The following would NOT compile โ€” different types despite same inner value:
    // let wrong: UserId = order_id;  // ERROR: mismatched types

    // Capability tokens
    let rt: Token<ReadPerm>  = Token::new(1);
    let wt: Token<WritePerm> = Token::new(2);

    println!("{}", read_resource(&rt, "config"));
    println!("{}", write_resource(&wt, "config", "debug=true"));

    // This would NOT compile:
    // read_resource(&wt, "x");    // ERROR: WritePerm โ‰  ReadPerm
    // write_resource(&rt, "x", "y"); // ERROR: ReadPerm โ‰  WritePerm

    println!("\nPhantomData size: {} bytes", std::mem::size_of::<PhantomData<User>>());
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn user_id_and_order_id_same_value_different_types() {
        let uid: UserId  = Id::new(1);
        let oid: OrderId = Id::new(1);
        assert_eq!(uid.value(), oid.value()); // same value, different type
    }

    #[test]
    fn token_read_perm() {
        let tok: Token<ReadPerm> = Token::new(42);
        let s = read_resource(&tok, "file.txt");
        assert!(s.contains("42"));
        assert!(s.contains("file.txt"));
    }

    #[test]
    fn token_write_perm() {
        let tok: Token<WritePerm> = Token::new(7);
        let s = write_resource(&tok, "db", "data");
        assert!(s.contains("7"));
        assert!(s.contains("data"));
    }

    #[test]
    fn phantom_data_is_zero_sized() {
        assert_eq!(std::mem::size_of::<PhantomData<String>>(), 0);
        assert_eq!(std::mem::size_of::<PhantomData<Vec<u8>>>(), 0);
    }

    #[test]
    fn id_is_same_size_as_u64() {
        assert_eq!(std::mem::size_of::<UserId>(), std::mem::size_of::<u64>());
    }
}
(* 738: Phantom Type Basics โ€” OCaml *)

(* Phantom type to distinguish read vs write tokens *)
type read_perm  = Read
type write_perm = Write

(* A capability token โ€” the 'perm phantom carries the permission *)
type 'perm token = Token of int   (* just an id *)

let make_read_token id  : read_perm token  = Token id
let make_write_token id : write_perm token = Token id

let token_id (Token id) = id

(* Operations gated by permission type *)
let read_data (tok : read_perm token) data =
  Printf.printf "Reading with token %d: %s\n" (token_id tok) data

let write_data (tok : write_perm token) data =
  Printf.printf "Writing with token %d: %s\n" (token_id tok) data

let () =
  let rt = make_read_token 1 in
  let wt = make_write_token 2 in
  read_data rt "hello";
  write_data wt "world";
  (* read_data wt "error" โ† type error: write_perm token โ‰  read_perm token *)
  Printf.printf "Tokens are type-safe!\n"