Part 3 · The compile-time machine II — types & traits
Traits are constraints, not types
A trait is not an interface and not a type. It is a predicate about types, and the difference explains coherence, the orphan rule, and why `impl` blocks are separate from `struct` definitions.
What this lesson is trying to break (4)
- A trait is Rust's word for an interface.
- You can use a trait as a type, like `Vec<Display>`.
- If a type has the right methods, it implements the trait — like Go or TypeScript.
- The orphan rule is arbitrary bureaucracy.
This is a threshold concept: troublesome, transformative, and irreversible. You should expect to be genuinely stuck here, and being stuck is the mechanism rather than a sign that you are failing. Budget several sessions. Do not move on because you are bored — move on when you can explain it out loud without notes.
Part 3 is the trait solver: the second machine from 0.2, the one that runs before your program does.
This lesson fixes the framing everything else depends on.
A trait is Rust’s word for an interface. It is a type that describes a
set of methods, and any object with those methods can be used where that type
is expected. So Vec of some trait should hold anything implementing it.
A trait is not a type. It has no size, no layout, and no
values. You cannot declare a variable of a trait, put one in a Vec, or
return one. Vec<Display> does not compile, and the error message
saying “trait objects must include the dyn keyword” is telling
you that you named the wrong kind of thing.
A trait is a predicate over types — a constraint. T: Display
is a claim about T, in the same way x > 0 is a claim about
x. Traits appear in bounds, not in type positions. The type you build
from a trait is dyn Trait, and it is a separate construct with its
own cost and its own rules (3.3).
This is not pedantry. Once you read T: Display as a constraint on a type
variable rather than as T is a subtype of Display, generic signatures stop
looking like object-oriented code with strange syntax, and the whole of
3.6 becomes readable.
Nominal and explicit
Varies: what makes a type count as implementing the interface
| Language | How a type qualifies | Consequence |
|---|---|---|
| TypeScript | Structurally — the shape matches | Accidental conformance is common, and usually harmless |
| Go | Structurally — the method set matches, no declaration | A type can satisfy an interface its author never heard of |
| Java | Nominally — implements Foo on the class | You cannot add an interface to a class you do not own |
| Rust | Nominally — impl Foo for Bar, anywhere the orphan rule permits | Explicit intent, and you can implement your trait for foreign types |
Rust takes the explicitness of Java but decouples the impl from the type definition, which is what makes extension traits possible.
That last row is the interesting combination. In Java, the set of interfaces a
class implements is fixed by whoever wrote the class. In Rust, the impl block is
a separate item, so you can implement your own trait for String, u32, or
someone else’s struct:
trait Shout { fn shout(&self) -> String; }
impl Shout for str {
fn shout(&self) -> String { self.to_uppercase() + "!" }
}
// now: "hello".shout()
This is the extension trait pattern, and it is everywhere in the ecosystem —
itertools::Itertools adds dozens of methods to every iterator, futures::StreamExt
does the same for streams.
use itertools::Itertools; · use futures::StreamExt;
A use whose imported name is never mentioned again. That is an extension
trait: it is imported purely so its methods become callable on existing types.
If you are reading code and a method does not appear in the type’s own docs,
look at the imports. That is where it came from. This is one of the most common
“where on earth is this method defined?” moments in Rust, and the answer is
almost always an extension trait in the use list.
Coherence and the orphan rule
Because impls are separate from type definitions, a question arises that Java never has to answer: what if two crates both implement the same trait for the same type?
Rust’s answer is coherence: for any trait/type pair there is exactly one implementation in the whole program. Not one per crate — one, globally.
The orphan rule enforces it locally: to write impl Trait for Type, at least
one of Trait or Type must be defined in your crate.
use std::fmt::Display;
struct MyType;
trait MyTrait { fn go(&self); }
// A
impl Display for MyType { /* ... */ }
// B
impl MyTrait for String { fn go(&self) {} }
// C
impl Display for Vec<u8> { /* ... */ }
// D
impl MyTrait for Vec<MyType> { fn go(&self) {} }A, B and D compile. C does not.
- A — your type, foreign trait. Fine:
MyTypeis local. - B — your trait, foreign type. Fine:
MyTraitis local. - C —
Displayisstd,Vecisstd. Neither is yours. E0117. - D —
MyTraitis local, so it is allowed regardless of what it is implemented for.
The reason C is forbidden is not bureaucracy. Suppose it were allowed. Crate a
implements Display for Vec<u8> one way; crate b implements it another way.
Both compile in isolation. Then some poor application depends on both — and now
vec.to_string() has two candidate implementations, chosen by nothing.
There is no good answer at that point. Compilation must fail, and the failure would be in a crate that did nothing wrong, caused by two dependencies that never knew about each other. This is sometimes called the “hashtable problem” and it is a real, unfixable-after-the-fact hazard in languages that allow it.
The standard workaround is the newtype pattern:
struct Bytes(Vec<u8>);
impl Display for Bytes { /* ... */ } // Bytes is local — allowedThat is why newtypes are so common in Rust. They are not obsessive type safety for its own sake; they are frequently the only way to attach behaviour to a foreign type.
Blanket impls
Because a trait is a predicate, you can implement it for every type satisfying another predicate:
impl<T: Display> ToString for T {
fn to_string(&self) -> String { /* ... */ }
}
One impl, covering every Display type that exists or will ever be written. That
is a blanket impl, and it is a genuinely different capability from anything in
Java or Go.
It is also why you should almost never implement ToString yourself — implement
Display and get ToString free. A reviewer noticing that is a small but real
signal of familiarity.
The cost is that blanket impls interact with coherence. Adding one to a published crate can break downstream code that already had its own impl, which is why they are semver-sensitive and why library authors think hard before shipping one.
Associated types versus generic parameters
Both appear inside traits and they look similar. The difference is how many times a type may implement the trait.
trait Iterator {
type Item; // associated type
fn next(&mut self) -> Option<Self::Item>;
}
trait From<T> { // generic parameter
fn from(value: T) -> Self;
}
Iterator::Itemis associated, so a type implementsIteratorat most once. That is right: aVec<u8>iterator yieldsu8, and there is no second sensible answer.From<T>is parameterised, so a type can implement it many times.StringimplementsFrom<&str>,From<char>,From<Box<str>>and more.
The rule of thumb for reading: associated type means “one answer per type”; generic parameter means “many answers allowed”. When you see a trait with an associated type, you know the author considered it unique.
You will also see associated types written as constraints:
fn sum_all<I>(iter: I) -> u32
where
I: Iterator<Item = u32>, // constrain the associated type
{ iter.sum() }
Iterator<Item = u32> is not a generic argument — it is a constraint on the
associated type, using = rather than a bare parameter. Recognising that syntax
is worth a lot when reading real signatures.
Supertraits and default methods
trait Animal {
fn name(&self) -> String;
fn greet(&self) -> String { // default method
format!("Hello, {}", self.name())
}
}
trait Pet: Animal { // supertrait
fn owner(&self) -> String;
}
trait Pet: Animal reads “anything implementing Pet must also implement
Animal”. It looks like inheritance and is not — there is no shared data and no
overriding; it is a requirement, one predicate implying another.
Default methods are how traits like Iterator provide ninety methods while
requiring implementors to write only next. When you read a big trait, the
question is always: which methods are required and which have defaults? The
docs mark this clearly, and it tells you how much work implementing it is.
A colleague from Go says:
“Why do I have to write
impl Display for MyTypeat all? My type has afmtmethod. Go would just accept it.”
Answer them. Cover: what explicitness buys, and what capability Rust gains that Go structurally cannot have. (Hint for the second part: think about what the orphan rule is protecting, and whether Go could have blanket impls.)
What you should now be able to say
- A trait is a predicate over types, not a type.
dyn Traitis the type. - Implementation is nominal and explicit, but the
implblock is separate from the type — which enables extension traits. - Coherence: one impl per trait/type pair, globally. The orphan rule enforces it; newtype is the workaround.
- Blanket impls implement a trait for every type satisfying another bound.
- Associated type = one answer per type. Generic parameter = many.
trait A: Bis a requirement, not inheritance.
Next: 3.3 — Dispatch and monomorphisation, where constraints turn into machine code and the costs become visible.