Skip to content

← Generics and Traits step 20 of 24

Hard Primitives

The orphan rule and the newtype escape

This is the single most-complained-about restriction in Rust, and the complaint is always the same shape: “I just want to impl Display for Vec<i32>, why won’t you let me?”

error[E0117]: only traits defined in the current crate can be implemented
              for types defined outside of the crate
   |
 5 | impl fmt::Display for Vec<i32> {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   | |                     |
   | |                     `Vec` is not defined in the current crate
   | impl doesn't use only types from inside the current crate

Why the rule exists

Trait resolution has to be coherent: for any (trait, type) pair there must be at most one impl in the whole program, and which one it is must not depend on link order or on which crates happen to be present.

Now imagine the rule did not exist. Crate pretty writes impl Display for Vec<i32> rendering [1, 2, 3]. Crate terse writes impl Display for Vec<i32> rendering 1 2 3. Your program depends on both. Which one runs? There is no answer that is not arbitrary, and worse, adding an unrelated dependency could silently change your output.

So Rust requires that for impl Trait for Type, at least one of the two must be local to your crate. Whoever owns the trait, or whoever owns the type, is responsible for the impl — and nobody else can conflict with them. Once you see it as “who is allowed to make this decision”, the frustration turns into a design principle.

Your task

pub struct Wrap(pub Vec<i32>);
impl Display for Wrap { ... }

pub fn render(rows: Vec<Vec<i32>>) -> Vec<String>

The newtype escape: wrap the foreign type in a local one, and now the Self half of the impl is yours. Wrap is a one-field tuple struct with the same layout as the Vec inside it — the wrapper costs nothing at runtime.

Each row renders as its Debug form:

[1, 2, 3]
[]
[-1]

Note the space after each comma; that is Debug for slices, not something you format yourself. Delegating Display to {:?} of the inner value is perfectly reasonable here.

E0117 vs E0210 — learners conflate these

Both are orphan-rule errors and they are not the same.

E0117 — “only traits defined in the current crate can be implemented for types defined outside of the crate”. Foreign trait, foreign type. Nothing in the impl is yours.

impl Display for Vec<i32> {}          // E0117

E0210 — “type parameter T must be used as the type parameter for some local type”. Here the Self type is local in a sense, but a type parameter appears in an uncovered position, so the impl could still collide with a future upstream impl.

impl<T> From<T> for MyWrapper { }     // E0210: T is uncovered

The formal statement, which is worth reading once even if it does not stick: for impl<P...> Trait<T1..Tn> for T0, either Trait is local, or at least one Ti is a local type with no uncovered type parameters preceding it. “Uncovered” means a bare T not wrapped in a local type constructor.

One special case you will meet in real code: Box and a few other types are fundamental, meaning Box<LocalType> counts as local for orphan purposes. That is why impl Trait for Box<MyType> works when impl Trait for Vec<MyType> does not.

A framing limit of this harness

Everything you write here lives in one crate, so strictly speaking nothing you do is ever “foreign” to a different crate of yours. The rule can only be demonstrated against std types — which is exactly what this problem does. In a real multi-crate project the same rule applies at every crate boundary, and it is what makes it safe for you to depend on two libraries that have never heard of each other.

What the newtype costs

Wrapping is not free in ergonomics, only in runtime:

  • Wrap has none of Vec‘s inherent methods. w.len() does not compile unless you write w.0.len() or add delegating methods.
  • You can add Deref<Target = Vec<i32>> to get them all back — and you have just read a whole problem about why that is usually a bad idea. For a wrapper whose point is to change how the type behaves, deref-ing to the inner type undoes the point.
  • The usual middle path is to implement exactly the traits you need (Display, IntoIterator, From<Vec<i32>>) and delegate a handful of methods by hand.

And when you write that From impl, remember clippy::from_over_into: implement From<Vec<i32>> for Wrap, never Into<Wrap> for Vec<i32> — which would be E0117 anyway, since both halves would be foreign.

Remember the grade is compile + tests + clippy -D warnings.