Skip to content

Rust Advanced Traits

On the Traits page you learned how to define a trait and implement it. On the Generics and Common Traits pages you used traits like Display, From, and Add. That is enough for most everyday code. But when you read the standard library, or a popular crate, you’ll see trait features that look a bit strange at first: type Item, Add<Rhs = Self>, trait Pretty: Display, impl<T: Display> ... for T.

This page explains those features one at a time. None of them are magic. Each one solves a small, real problem, and once you know the problem, the syntax makes sense. Take it slowly — it’s fine to read one section, try the code, and come back later.

You’ve already met an associated type: Iterator has type Item. An associated type is a placeholder type inside a trait. Each implementation fills it in once.

Here is a small Container trait. A playlist holds Strings and a score list holds u32s, so each picks its own Item:

use std::fmt::Debug;
trait Container {
type Item;
fn get(&self, index: usize) -> Option<&Self::Item>;
fn size(&self) -> usize;
fn first(&self) -> Option<&Self::Item> {
self.get(0)
}
}
struct Playlist {
songs: Vec<String>,
}
struct Scores {
values: Vec<u32>,
}
impl Container for Playlist {
type Item = String;
fn get(&self, index: usize) -> Option<&String> {
self.songs.get(index)
}
fn size(&self) -> usize {
self.songs.len()
}
}
impl Container for Scores {
type Item = u32;
fn get(&self, index: usize) -> Option<&u32> {
self.values.get(index)
}
fn size(&self) -> usize {
self.values.len()
}
}
// Works with any Container whose items can be printed with {:?}
fn describe<C: Container>(c: &C)
where
C::Item: Debug,
{
println!("{} items, first is {:?}", c.size(), c.first());
}
fn main() {
let playlist = Playlist { songs: vec![String::from("Intro"), String::from("Outro")] };
let scores = Scores { values: vec![90, 72, 85] };
describe(&playlist);
describe(&scores);
}

Output:

2 items, first is Some("Intro")
3 items, first is Some(90)
▶ Try it Yourself

Inside the trait, Self::Item means “whatever type this implementation chose”. C::Item in describe means the same thing for the type C. (The where line is just a tidier place to write the bound — more on that at the end of this page.)

Why not a generic parameter instead? You could write trait Container<T>. But then one type could implement it many times — Container<String> and Container<u32> — and every caller would have to say which one they mean. For a playlist that makes no sense: it holds songs, full stop.

That’s exactly why Iterator uses type Item. A Vec<i32> iterator yields i32 and nothing else, so for x in v.iter() never needs a type annotation. A simple rule of thumb:

  • One natural answer per type → associated type (Iterator::Item, Add::Output).
  • Many valid answers per type → generic parameter (From<T>: a String can be built from a &str, a char, a Box<str>, …).

On the Common Traits page you made + work for a Point by implementing Add. Here is how Add is really defined in the standard library:

trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}

Two features meet here. Output is an associated type (what the + produces). And Rhs = Self is a default type parameter: “the right-hand side is the same type as the left, unless you say otherwise”. When you write impl Add for Point, you get the default. When you want to add a different type, you write impl Add<OtherType> for Point.

use std::ops::Add;
#[derive(Debug, Clone, Copy)]
struct Meters(f64);
#[derive(Debug, Clone, Copy)]
struct Centimeters(f64);
// Uses the default Rhs = Self, so this is Meters + Meters
impl Add for Meters {
type Output = Meters;
fn add(self, other: Meters) -> Meters {
Meters(self.0 + other.0)
}
}
// Overrides the default: Meters + Centimeters
impl Add<Centimeters> for Meters {
type Output = Meters;
fn add(self, other: Centimeters) -> Meters {
Meters(self.0 + other.0 / 100.0)
}
}
fn main() {
let a = Meters(2.0);
let b = Meters(3.5);
let c = Centimeters(50.0);
println!("{:?}", a + b);
println!("{:?}", a + c);
}

Output:

Meters(5.5)
Meters(2.5)
▶ Try it Yourself

Notice that Add uses both tools: a generic parameter for the right-hand side (many valid answers — you can add Meters or Centimeters) and an associated type for the result (one answer for each pair). Default type parameters are mostly useful when you design a trait: they keep the common case short without blocking the rare case.

Supertraits: “you must also implement this”

Section titled “Supertraits: “you must also implement this””

Sometimes your trait only makes sense if the type can already do something else. A supertrait says “to implement my trait, you must also implement that one”. You write it with a colon, like a bound:

use std::fmt;
// Anything that is Pretty must also be Display
trait Pretty: fmt::Display {
fn pretty(&self) -> String {
format!("*** {} ***", self)
}
}
struct Temperature {
celsius: f64,
}
impl fmt::Display for Temperature {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.1}°C", self.celsius)
}
}
impl Pretty for Temperature {}
fn main() {
let t = Temperature { celsius: 21.456 };
println!("{}", t);
println!("{}", t.pretty());
}

Output:

21.5°C
*** 21.5°C ***
▶ Try it Yourself

The default pretty method can use {} on self because the supertrait promises Display is there. Without : fmt::Display, Rust would refuse that format!, since it couldn’t know the type is printable. impl Pretty for Temperature {} is empty — it just says “yes, use the default method”.

Real examples from the standard library: Copy: Clone (every Copy type must also be Clone) and Eq: PartialEq.

Blanket implementations: one impl for many types

Section titled “Blanket implementations: one impl for many types”

A blanket implementation implements a trait for every type that meets a condition, in one go. The syntax is impl<T: SomeBound> MyTrait for T:

use std::fmt::Display;
trait Shout {
fn shout(&self) -> String;
}
// Every type that implements Display gets Shout for free
impl<T: Display> Shout for T {
fn shout(&self) -> String {
format!("{}!", self.to_string().to_uppercase())
}
}
fn main() {
println!("{}", "hello".shout());
println!("{}", 42.shout());
println!("{}", 'a'.shout());
println!("{}", String::from("rust is fun").shout());
}

Output:

HELLO!
42!
A!
RUST IS FUN!
▶ Try it Yourself

We never wrote impl Shout for i32 or impl Shout for char. The blanket impl covered them all — including Display types that don’t exist yet.

You’ve used a blanket impl many times without knowing it. The standard library has (roughly) impl<T: Display> ToString for T. That’s why every type with Display has a .to_string() method. The same trick gives you Into for free when you implement From.

One limit: once a blanket impl exists, you can’t add a second, more specific impl for a type it already covers (say, a special impl Shout for i32). Rust would have two answers and refuses with a “conflicting implementations” error.

The orphan rule and the newtype workaround

Section titled “The orphan rule and the newtype workaround”

Here’s a rule that surprises everyone once. Try to give Vec<String> a nice Display:

use std::fmt;
impl fmt::Display for Vec<String> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.join(", "))
}
}
error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
--> src/main.rs:3:1
|
3 | impl fmt::Display for Vec<String> {
| ^^^^^^^^^^^^^^^^^^^^^^-----------
| |
| `Vec` is not defined in the current crate
|
= note: define and implement a trait or new type instead

This is the orphan rule: you can implement a trait for a type only if the trait or the type is defined in your crate. Display belongs to the standard library, and so does Vec, so neither is yours.

Why does the rule exist? Imagine two crates you depend on both wrote their own impl Display for Vec<String>. Which one should Rust use? There’s no good answer, so Rust makes the situation impossible.

The workaround is the newtype pattern: wrap the foreign type in a tiny struct of your own. Now the type is yours, and you can implement anything for it:

use std::fmt;
// A local wrapper around a foreign type
struct Shopping(Vec<String>);
impl fmt::Display for Shopping {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let list = Shopping(vec![String::from("milk"), String::from("eggs"), String::from("bread")]);
println!("You need: {}", list);
println!("That's {} things", list.0.len());
}

Output:

You need: [milk, eggs, bread]
That's 3 things
▶ Try it Yourself

self.0 reaches the Vec inside. A newtype has no runtime cost — in memory, Shopping is exactly a Vec<String>. The downside is that the Vec methods aren’t on Shopping directly, so you write list.0.len() (or add the methods you need). Newtypes are useful for much more than the orphan rule; see Design Patterns.

Two traits can have methods with the same name, and the type itself can have one too. Which one does robot.greet() call?

trait English {
fn greet(&self) -> String;
fn language() -> String;
}
trait Spanish {
fn greet(&self) -> String;
fn language() -> String;
}
struct Robot;
impl Robot {
fn greet(&self) -> String {
String::from("BEEP BOOP")
}
}
impl English for Robot {
fn greet(&self) -> String { String::from("Hello!") }
fn language() -> String { String::from("English") }
}
impl Spanish for Robot {
fn greet(&self) -> String { String::from("¡Hola!") }
fn language() -> String { String::from("Spanish") }
}
fn main() {
let r = Robot;
println!("{}", r.greet()); // the type's own method wins
println!("{}", English::greet(&r)); // pick a trait by name
println!("{}", Spanish::greet(&r));
// No `self` here, so Rust needs the type AND the trait:
println!("{}", <Robot as English>::language());
println!("{}", <Robot as Spanish>::language());
}

Output:

BEEP BOOP
Hello!
¡Hola!
English
Spanish
▶ Try it Yourself

Three levels, from short to fully explicit:

  • r.greet() — the type’s own (inherent) method is chosen first.
  • English::greet(&r) — “the English version”. Rust works out the type from &r.
  • <Robot as English>::language() — fully qualified syntax. You need it for functions with no self, because there’s no value for Rust to look at. English::language() alone fails with error[E0790]: cannot call associated function on trait without specifying the corresponding impl type.

You won’t need this often. When you do, the compiler error usually suggests the exact fix.

impl Trait in arguments and return values, plus where

Section titled “impl Trait in arguments and return values, plus where”

You can write impl Trait in two places, and it means something slightly different in each:

  • Argument position — fn f(x: impl Trait): the caller picks the type. It’s a short way to write a generic fn f<T: Trait>(x: T).
  • Return position — fn f() -> impl Trait: the function picks one concrete type and keeps it hidden. The caller only knows “it’s something that implements Trait”.
// Caller chooses: any iterator of u32 is fine
fn print_all(items: impl Iterator<Item = u32>) {
let all: Vec<u32> = items.collect();
println!("{:?}", all);
}
// Function chooses: the real type is a long Filter<...> we never write out
fn evens_up_to(limit: u32) -> impl Iterator<Item = u32> {
(0..=limit).filter(|n| n % 2 == 0)
}
fn main() {
print_all(vec![3, 1, 4].into_iter());
print_all(evens_up_to(10));
}

Output:

[3, 1, 4]
[0, 2, 4, 6, 8, 10]
▶ Try it Yourself

Return-position impl Trait is especially handy with closures and iterators. The real type of evens_up_to’s result includes the closure’s type, and closures have no name you could type. impl Iterator lets you skip it.

When a function has several generic parameters with several bounds each, the angle brackets get crowded. A where clause moves the bounds after the signature, so the function’s shape is easier to read:

use std::fmt::{Debug, Display};
// Crowded: fn report<T: Display, U: Debug + PartialOrd>(label: T, a: U, b: U) -> String
// Tidier:
fn report<T, U>(label: T, a: U, b: U) -> String
where
T: Display,
U: Debug + PartialOrd,
{
let bigger = if a > b { a } else { b };
format!("{}: bigger is {:?}", label, bigger)
}
fn main() {
println!("{}", report("scores", 7, 12));
println!("{}", report(String::from("names"), "ann", "bob"));
}

Output:

scores: bigger is 12
names: bigger is "bob"
▶ Try it Yourself

Both forms mean exactly the same thing to the compiler. where is also the natural place for bounds on associated types, like C::Item: Debug in the first example on this page.

Implementing a trait but forgetting its supertrait. If you remove the impl fmt::Display for Temperature block from the supertrait example, you get:

error[E0277]: `Temperature` doesn't implement `std::fmt::Display`
--> src/main.rs:13:17
|
13 | impl Pretty for Temperature {}
| ^^^^^^^^^^^ unsatisfied trait bound
|
note: required by a bound in `Pretty`
--> src/main.rs:3:15
|
3 | trait Pretty: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `Pretty`

The fix is to implement the supertrait (here, Display) too. The note points right at the : fmt::Display that requires it.

Returning two different types from -> impl Trait. impl Trait hides one type. It can’t be “sometimes this, sometimes that”:

fn numbers(evens: bool) -> impl Iterator<Item = u32> {
if evens {
(0..10).filter(|n| n % 2 == 0)
} else {
vec![1, 3, 5].into_iter()
}
}
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:5:9
|
2 | / if evens {
3 | | (0..10).filter(|n| n % 2 == 0)
| | ------------------------------
| | | |
| | | the expected closure
| | expected because of this
4 | | } else {
5 | | vec![1, 3, 5].into_iter()
| | ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Filter<Range<{integer}>, {closure@...}>`, found `IntoIter<{integer}>`
6 | | }
| |_____- `if` and `else` have incompatible types
|
= note: expected struct `Filter<std::ops::Range<{integer}>, {closure@src/main.rs:3:24: 3:27}>`
found struct `std::vec::IntoIter<{integer}>`
help: you could change the return type to be a boxed trait object
|
1 - fn numbers(evens: bool) -> impl Iterator<Item = u32> {
1 + fn numbers(evens: bool) -> Box<dyn Iterator<Item = u32>> {
|
help: if you change the return type to expect trait objects, box the returned expressions
|
3 ~ Box::new((0..10).filter(|n| n % 2 == 0))
4 | } else {
5 ~ Box::new(vec![1, 3, 5].into_iter())
|

When the type really changes at runtime, return a Box<dyn Trait> instead, as the compiler suggests. See Trait Objects.

  • An associated type (type Item) gives one answer per implementation. Use a generic parameter when a type can implement the trait many ways.
  • Default type parameters like Add<Rhs = Self> keep the common case short; impl Add<Other> for T overrides them.
  • A supertrait (trait Pretty: Display) requires another trait, and lets default methods use it.
  • A blanket impl (impl<T: Display> MyTrait for T) covers every matching type at once — that’s how .to_string() works.
  • The orphan rule stops you implementing a foreign trait for a foreign type. Wrap the type in a newtype to get around it.
  • Use fully qualified syntax <Type as Trait>::function() when names clash; impl Trait in return position hides one concrete type; where makes long bounds readable.

Quick check

1. Why does Iterator use type Item instead of Iterator<T>?

2. What does Rhs = Self mean in trait Add<Rhs = Self>?

3. You want impl Display for Vec<String>. Rust refuses. What's the usual fix?

4. Which line calls the English version of a function with no self parameter?

5. In fn evens() -> impl Iterator<Item = u32>, who decides the concrete iterator type?

Score: 0 / 5