🦀 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

100: Phantom Types — Type-Safe Units

Difficulty: 3 Level: Intermediate Prevent mixing meters with seconds at compile time — zero runtime overhead, pure type system enforcement.

The Problem This Solves

Unit errors cause real disasters. NASA lost the Mars Climate Orbiter because one team used metric units and another used imperial. In code, `add(distance_m, time_s)` looks fine to the compiler if both are `f64` — but it's physically nonsense. Runtime validation catches this too late. You want the compiler to refuse `meters(100.0) + seconds(5.0)` the same way it refuses `"hello" + 42`. Both are type errors — but without phantom types, the compiler can't see it. The solution: tag each value with a marker type that tracks what it represents. The marker carries no data — it's purely for the type checker.

The Intuition

A `Quantity<Meters>` and a `Quantity<Seconds>` are different types even though they both contain a single `f64`. The `Meters` and `Seconds` types are never stored — they're just type-level labels. `PhantomData<T>` is Rust's way of saying "this struct is parameterized by T, but T doesn't appear in any stored field." Without it, the compiler would complain about unused type parameters. The size test confirms zero overhead: `size_of::<Quantity<Meters>>() == size_of::<f64>()`.

How It Works in Rust

use std::marker::PhantomData;
use std::ops::Add;

// The unit types — zero-sized markers, never constructed
pub struct Meters;
pub struct Seconds;

// A quantity tagged with its unit
#[derive(Debug, Clone, Copy)]
pub struct Quantity<Unit> {
 value: f64,
 _unit: PhantomData<Unit>,  // zero size, tells compiler about the type param
}

impl<U> Quantity<U> {
 pub fn new(value: f64) -> Self {
     Quantity { value, _unit: PhantomData }
 }
}

// Add is only defined when BOTH sides have the SAME unit
impl<U> Add for Quantity<U> {
 type Output = Self;
 fn add(self, rhs: Self) -> Self {
     Quantity::new(self.value + rhs.value)
 }
}

pub fn meters(v: f64) -> Quantity<Meters> { Quantity::new(v) }
pub fn seconds(v: f64) -> Quantity<Seconds> { Quantity::new(v) }
This code compiles:
let total = meters(100.0) + meters(50.0);   // OK: same unit
This doesn't:
let bad = meters(100.0) + seconds(5.0);     // Compile error: type mismatch
The compiler catches the bug — no runtime check, no overhead.

What This Unlocks

Key Differences

ConceptOCamlRust
Phantom markerAbstract type in signature: `type meters``PhantomData<Meters>` struct field
Runtime costZeroZero
Adding same unitsModule functor or direct `+``impl Add for Quantity<U>`
Cross-unit safetyModule abstractionType mismatch at compile time
AlternativeN/ANewtype wrapper (`struct Meters(f64)`) — simpler but less flexible
//! # Phantom Types — Type-Safe Units
//!
//! Use phantom type parameters to prevent mixing meters and seconds at compile time.
//! OCaml's abstract types map to Rust's `PhantomData<T>` marker.

use std::marker::PhantomData;
use std::ops::Add;

// ---------------------------------------------------------------------------
// Approach A: PhantomData marker (idiomatic Rust)
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy)]
pub struct Quantity<Unit> {
    value: f64,
    _unit: PhantomData<Unit>,
}

#[derive(Debug)]
pub struct Meters;
#[derive(Debug)]
pub struct Seconds;

impl<U> Quantity<U> {
    pub fn new(value: f64) -> Self {
        Quantity { value, _unit: PhantomData }
    }

    pub fn value(&self) -> f64 {
        self.value
    }

    pub fn scale(&self, k: f64) -> Self {
        Quantity::new(k * self.value)
    }
}

impl<U> Add for Quantity<U> {
    type Output = Self;
    fn add(self, rhs: Self) -> Self {
        Quantity::new(self.value + rhs.value)
    }
}

pub fn meters(v: f64) -> Quantity<Meters> { Quantity::new(v) }
pub fn seconds(v: f64) -> Quantity<Seconds> { Quantity::new(v) }

// ---------------------------------------------------------------------------
// Approach B: Newtype wrappers (simpler, no PhantomData)
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MetersVal(pub f64);

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SecondsVal(pub f64);

impl Add for MetersVal {
    type Output = Self;
    fn add(self, rhs: Self) -> Self { MetersVal(self.0 + rhs.0) }
}

impl Add for SecondsVal {
    type Output = Self;
    fn add(self, rhs: Self) -> Self { SecondsVal(self.0 + rhs.0) }
}

// ---------------------------------------------------------------------------
// Approach C: Const generics (Rust-specific, experimental flavor)
// ---------------------------------------------------------------------------

// Using a string-based unit tag with const generics is nightly-only,
// but the concept shows Rust's direction for compile-time unit checking.

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

    #[test]
    fn test_add_same_units() {
        let d1 = meters(100.0);
        let d2 = meters(50.0);
        let total = d1 + d2;
        assert!((total.value() - 150.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_scale() {
        let t = seconds(3.0);
        let doubled = t.scale(2.0);
        assert!((doubled.value() - 6.0).abs() < f64::EPSILON);
    }

    // This should NOT compile — uncomment to verify:
    // #[test]
    // fn test_add_different_units_fails() {
    //     let d = meters(100.0);
    //     let t = seconds(5.0);
    //     let _ = d + t; // Compile error!
    // }

    #[test]
    fn test_newtype_add() {
        assert_eq!(MetersVal(10.0) + MetersVal(5.0), MetersVal(15.0));
    }

    #[test]
    fn test_phantom_zero_size() {
        assert_eq!(
            std::mem::size_of::<Quantity<Meters>>(),
            std::mem::size_of::<f64>()
        );
    }

    #[test]
    fn test_multiple_operations() {
        let d = meters(10.0) + meters(20.0);
        let scaled = d.scale(3.0);
        assert!((scaled.value() - 90.0).abs() < f64::EPSILON);
    }
}

fn main() {
    println!("{:?}", (total.value() - 150.0).abs() < f64::EPSILON);
    println!("{:?}", (doubled.value() - 6.0).abs() < f64::EPSILON);
    println!("{:?}", MetersVal(10.0) + MetersVal(5.0), MetersVal(15.0));
}
type meters
type seconds
type 'a quantity = Q of float

let meters x : meters quantity = Q x
let seconds x : seconds quantity = Q x

let add (Q a : 'a quantity) (Q b : 'a quantity) : 'a quantity = Q (a +. b)
let scale k (Q a : 'a quantity) : 'a quantity = Q (k *. a)
let value (Q x) = x

let () =
  let d1 = meters 100.0 in
  let d2 = meters 50.0 in
  let total = add d1 d2 in
  Printf.printf "Distance: %.1f m\n" (value total);
  let doubled = scale 2.0 (seconds 3.0) in
  Printf.printf "Time: %.1f s\n" (value doubled)

📊 Detailed Comparison

Comparison: Phantom Types — OCaml vs Rust

Core Insight

Phantom types demonstrate zero-cost type safety in both languages. OCaml uses abstract types (declared but never defined) as phantom parameters. Rust uses `PhantomData<T>` — a zero-sized type from `std::marker`. In both cases, the compiler enforces that you can't add meters to seconds, but the runtime representation is just a float.

OCaml

🐪 Show OCaml equivalent
type meters
type seconds
type 'a quantity = Q of float

let meters x : meters quantity = Q x
let add (Q a : 'a quantity) (Q b : 'a quantity) : 'a quantity = Q (a +. b)

Rust

use std::marker::PhantomData;

pub struct Quantity<Unit> {
 value: f64,
 _unit: PhantomData<Unit>,
}

impl<U> Add for Quantity<U> {
 type Output = Self;
 fn add(self, rhs: Self) -> Self { Quantity::new(self.value + rhs.value) }
}

Comparison Table

AspectOCamlRust
Phantom type`type meters` (abstract)`struct Meters;` (zero-sized)
MarkerImplicit in type param`PhantomData<Unit>`
Runtime costZeroZero (`PhantomData` is ZST)
Operator overloadFunctions only`impl Add for Quantity<U>`
AlternativeModule signaturesNewtype wrappers
Size checkN/A`size_of::<Quantity<M>>() == size_of::<f64>()`

Learner Notes

  • `PhantomData`: Rust requires explicit marking because it tracks all type parameters for drop checking and variance
  • Zero-sized types (ZSTs): `struct Meters;` takes 0 bytes — the compiler optimizes it away entirely
  • Trait-based ops: Rust's `impl Add` gives `+` syntax; OCaml just uses named functions
  • Newtype alternative: `struct Meters(f64)` is simpler but requires implementing ops for each unit separately
  • OCaml's elegance: Abstract types + type annotations is more concise than Rust's PhantomData approach