397: Marker Traits
Tutorial Video
Text description (accessibility)
This video demonstrates the "397: Marker Traits" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Some type properties cannot be expressed as methods — they are structural guarantees about how a type behaves, not behaviors you can call. Key difference from OCaml: 1. **Methods**: Rust marker traits are truly empty (no methods); OCaml phantom types are also empty type parameters, but they require more infrastructure to encode constraints.
Tutorial
The Problem
Some type properties cannot be expressed as methods — they are structural guarantees about how a type behaves, not behaviors you can call. A type is "serializable" (safe to convert to bytes), "immutable" (guarantees no internal mutation), or "thread-safe" (safe to share across threads). Marker traits capture these properties: they have no methods, just a name. Code that requires a guarantee asks for T: Serializable in its bounds, and only explicitly-opted-in types pass through. This prevents accidentally passing a non-serializable type to a serialization function.
Marker traits include Copy, Send, Sync, Unpin, UnwindSafe, and user-defined invariants in domain-specific type systems.
🎯 Learning Outcomes
ThreadSafe: Send + Sync composes existing marker traits into a semantic conceptunsafe impl requirement for traits with safety invariants like Send/SyncCode Example
#![allow(clippy::all)]
//! Marker Traits
pub trait Serializable {}
pub trait Immutable {}
pub trait ThreadSafe: Send + Sync {}
#[derive(Clone)]
pub struct Config {
pub name: String,
}
impl Serializable for Config {}
impl Immutable for Config {}
pub struct Counter {
pub value: std::sync::atomic::AtomicU64,
}
impl ThreadSafe for Counter {}
unsafe impl Send for Counter {}
unsafe impl Sync for Counter {}
pub fn save<T: Serializable>(val: &T) -> String {
"saved".to_string()
}
pub fn process_threadsafe<T: ThreadSafe>(val: &T) -> String {
"processed".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serializable() {
let c = Config {
name: "test".into(),
};
assert_eq!(save(&c), "saved");
}
#[test]
fn test_threadsafe() {
let c = Counter {
value: std::sync::atomic::AtomicU64::new(0),
};
assert_eq!(process_threadsafe(&c), "processed");
}
#[test]
fn test_marker_has_no_methods() {
let _c = Config { name: "x".into() }; /* Marker traits have no methods */
}
}Key Differences
Send/Sync require unsafe impl to assert invariants the compiler can't verify; OCaml's module-based safety relies on hiding constructors, not unsafe declarations.Send/Sync for types where all fields are Send/Sync; OCaml phantom types must be manually propagated.OCaml Approach
OCaml achieves marker-like behavior through phantom types: type ('a, 'serializable) t = T of ... where 'serializable is a phantom parameter set to a serializable type or not_serializable. Module signatures achieve the same with abstract types that only appear in certain signatures. OCaml has no equivalent of unsafe impl — safety invariants are expressed through module abstraction hiding constructors.
Full Source
#![allow(clippy::all)]
//! Marker Traits
pub trait Serializable {}
pub trait Immutable {}
pub trait ThreadSafe: Send + Sync {}
#[derive(Clone)]
pub struct Config {
pub name: String,
}
impl Serializable for Config {}
impl Immutable for Config {}
pub struct Counter {
pub value: std::sync::atomic::AtomicU64,
}
impl ThreadSafe for Counter {}
unsafe impl Send for Counter {}
unsafe impl Sync for Counter {}
pub fn save<T: Serializable>(val: &T) -> String {
"saved".to_string()
}
pub fn process_threadsafe<T: ThreadSafe>(val: &T) -> String {
"processed".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serializable() {
let c = Config {
name: "test".into(),
};
assert_eq!(save(&c), "saved");
}
#[test]
fn test_threadsafe() {
let c = Counter {
value: std::sync::atomic::AtomicU64::new(0),
};
assert_eq!(process_threadsafe(&c), "processed");
}
#[test]
fn test_marker_has_no_methods() {
let _c = Config { name: "x".into() }; /* Marker traits have no methods */
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serializable() {
let c = Config {
name: "test".into(),
};
assert_eq!(save(&c), "saved");
}
#[test]
fn test_threadsafe() {
let c = Counter {
value: std::sync::atomic::AtomicU64::new(0),
};
assert_eq!(process_threadsafe(&c), "processed");
}
#[test]
fn test_marker_has_no_methods() {
let _c = Config { name: "x".into() }; /* Marker traits have no methods */
}
}
Deep Comparison
OCaml vs Rust: 397-marker-traits
Exercises
trait Validated {} and create a ValidatedEmail(String) that implements it, but only constructable via ValidatedEmail::new(s: &str) -> Option<ValidatedEmail>. Write a fn send_email<T: Validated + AsRef<str>>(addr: &T) that only accepts validated addresses.trait ReadPermission {} and trait WritePermission {}. Create a File<P> where P is a permission marker. Show that fn write<P: WritePermission>(f: &File<P>) rejects a read-only file at compile time.DatabaseType marker that only your crate's Postgres, Mysql, and Sqlite types can implement. Write a generic fn connect<D: DatabaseType>(config: &str).