ExamplesBy LevelBy TopicLearning Paths
397 Intermediate

397: Marker Traits

Functional Programming

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

  • • Understand marker traits as zero-method types expressing structural invariants
  • • Learn how marker trait bounds gate access to functions at compile time
  • • See how ThreadSafe: Send + Sync composes existing marker traits into a semantic concept
  • • Understand when to use marker traits vs. methods (invariants vs. behaviors)
  • • Learn the unsafe impl requirement for traits with safety invariants like Send/Sync
  • Code 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

  • 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.
  • Unsafe impl: Rust's 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.
  • Auto-derivation: Rust auto-derives Send/Sync for types where all fields are Send/Sync; OCaml phantom types must be manually propagated.
  • Documentation: Rust marker traits appear in rustdoc and IDE autocompletion, making them visible; OCaml phantom type constraints require reading type signatures carefully.
  • 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 */
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Validated marker: Define 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.
  • Permission markers: Define 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.
  • Sealed marker: Combine the sealed trait pattern with markers: define a sealed DatabaseType marker that only your crate's Postgres, Mysql, and Sqlite types can implement. Write a generic fn connect<D: DatabaseType>(config: &str).
  • Open Source Repos