Reading Rust

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.

45 minThreshold conceptrefutation textcontrasting casesPOE
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.
Liminality warning

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.

Unlearn thiscarried over from Java interfaces, Go interfaces, TypeScript structural types
You think

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.

Wrong here

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&lt;Display&gt; 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.

Instead

A trait is a predicate over types — a constraint. T: Display is a claim about T, in the same way x &gt; 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

LanguageHow a type qualifiesConsequence
TypeScriptStructurally — the shape matchesAccidental conformance is common, and usually harmless
GoStructurally — the method set matches, no declarationA type can satisfy an interface its author never heard of
JavaNominally — implements Foo on the classYou cannot add an interface to a class you do not own
RustNominally — impl Foo for Bar, anywhere the orphan rule permitsExplicit 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.

Beaconsee this → think this, without decoding it

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.

Predict firstcommit in writing, then look
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) {} }

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::Item is associated, so a type implements Iterator at most once. That is right: a Vec<u8> iterator yields u8, and there is no second sensible answer.
  • From<T> is parameterised, so a type can implement it many times. String implements From<&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.

Explain it to yourselfwrite it — thinking it does not count

A colleague from Go says:

“Why do I have to write impl Display for MyType at all? My type has a fmt method. 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 Trait is the type.
  • Implementation is nominal and explicit, but the impl block 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: B is a requirement, not inheritance.

Next: 3.3 — Dispatch and monomorphisation, where constraints turn into machine code and the costs become visible.