In this lesson, you will learn how to use empty traits as "tags" to communicate specific capabilities of your data types to the compiler and your API users.
The Concept of a Marker Trait
A marker trait is a trait that has no methods or associated types. Because it contains no logic, you don't implement functions for it; you simply declare that a type satisfies the trait. Think of it as a "stamp of approval" that you place on a struct to tell the world, "This type possesses a specific capability."
Signal Capabilities at Compile Time
Suppose you are building a system for a high-security document storage application. You have a Document<T> struct, but some documents are Encrypted and others are Plaintext. You want to ensure that only Encrypted documents can be passed into a specific decrypt() function.
Instead of checking a boolean flag at runtime, you can define marker traits:
trait Encrypted {}
trait Plaintext {}
struct Document<State> {
content: String,
_marker: std::marker::PhantomData<State>,
}
fn decrypt(doc: Document<impl Encrypted>) -> Document<Plaintext> {
// Perform decryption logic here...
Document { content: "decrypted".to_string(), _marker: std::marker::PhantomData }
}
In this example, Encrypted and Plaintext are markers. They carry zero runtime cost because they are erased during compilation. However, they allow the compiler to enforce strict rules: if a developer tries to pass a Document<Plaintext> into the decrypt function, the compiler will stop them immediately because the Document does not implement the Encrypted marker.
Why use this pattern?
Using markers allows you to move logic from runtime checks (which can fail while the program is running) to compile-time guarantees (which prevent the program from even building if the logic is wrong). It transforms your data types into self-documenting capabilities that signal to any consumer exactly what operations are permissible on that data.
Key Takeaway: Marker traits provide a zero-cost mechanism to tag types with specific capabilities, allowing you to leverage the compiler to enforce design constraints and state safety at compile time.