Traits — The basics

Traits — The basics

1. What is a trait

A trait is a set of methods that a type promises to provide. It is the equivalent of interfaces in Java or TypeScript, with two differences:

  • you can implement a trait for a type after the fact, without changing its definition (even i32 or String);
  • a trait can provide default code.
trait Describe {
    fn describe(&self) -> String;
}

struct Dog { name: String }
struct Car { model: String }

impl Describe for Dog {
    fn describe(&self) -> String { format!("the dog {}", self.name) }
}

impl Describe for Car {
    fn describe(&self) -> String { format!("a {}", self.model) }
}

fn introduce(x: &impl Describe) {
    println!("Here is {}", x.describe());
}

Dog and Car have nothing in common, except this behavior. introduce accepts any type that has it.


2. Defining a trait

trait Shape {
    // Method to implement: only the signature
    fn area(&self) -> f64;

    // Method that modifies
    fn scale(&mut self, factor: f64);

    // Associated function (no self): a common constructor
    fn unit() -> Self where Self: Sized;

    // Associated constant
    const SIDES: u32;

    // Default method (section 4)
    fn description(&self) -> String {
        format!("shape with {} sides, area {:.2}", Self::SIDES, self.area())
    }
}
ElementSyntaxCall
methodfn f(&self)value.f()
method that modifiesfn f(&mut self)value.f()
method that consumesfn f(self)value.f()
associated functionfn f() -> SelfType::f() or T::f()
constantconst N: u32;Type::N or T::N

3. Implementing a trait

struct Square { side: f64 }

impl Shape for Square {
    fn area(&self) -> f64 { self.side * self.side }
    fn scale(&mut self, factor: f64) { self.side *= factor; }
    fn unit() -> Self { Square { side: 1.0 } }
    const SIDES: u32 = 4;
    // description is not written: the default version applies
}

fn main() {
    let mut c = Square::unit();
    c.scale(3.0);
    println!("{}", c.description());   // shape with 4 sides, area 9.00
}

All methods without a body must be written, with exactly the same signature. The compiler lists the missing ones (missing ... in implementation).

The `impl Trait for Type` block is separate from the `impl Type` block

impl Square {
    fn diagonal(&self) -> f64 { self.side * 2f64.sqrt() }   // method specific to Square
}

impl Shape for Square {
    // only the items declared by the trait: area, scale, unit, SIDES
    // (and possibly description, to replace the default version)
}

You cannot add an extra method inside an impl Trait for Type: it goes in impl Type.


4. Default methods

4.1 A default method can call the others

trait Summary {
    fn author(&self) -> String;

    fn summary(&self) -> String {
        format!("(Read more from {}…)", self.author())
    }
}

struct Tweet { user: String, text: String }

impl Summary for Tweet {
    fn author(&self) -> String { format!("@{}", self.user) }
    // summary is provided
}

The trait asks for the minimum (author) and builds the rest. This is the principle of Iterator: you write next, you get map, filter, sum and 70 other methods.

4.2 Replacing a default method

struct Article { title: String, author: String }

impl Summary for Article {
    fn author(&self) -> String { self.author.clone() }
    fn summary(&self) -> String {                      // replaces the default version
        format!("{}, by {}", self.title, self.author)
    }
}

An implementation that replaces the default method cannot call the default version (there is no super). To reuse it, put it in a free function that both call.

4.3 Designing a trait

Ask for as few required methods as possible, and provide by default everything that can be deduced from them. Adding a method with a default later breaks nobody; adding a required method breaks all existing implementations.


5. Using a trait: parameters and return values

5.1 As a parameter: the same function in four ways

// 1. impl Trait
fn notify_v1(x: &impl Summary) {
    println!("{}", x.summary());
}

// 2. Generic with a bound
fn notify_v2<T: Summary>(x: &T) {
    println!("{}", x.summary());
}

// 3. where
fn notify_v3<T>(x: &T)
where
    T: Summary,
{
    println!("{}", x.summary());
}

// 4. Trait object: a single compiled copy, choice at runtime
fn notify_v4(x: &dyn Summary) {
    println!("{}", x.summary());
}

The first three are equivalent (generics, see generics). The fourth is dynamic (see box-dynamiques).

5.2 Several bounds

use std::fmt::Display;

fn show_summary(x: &(impl Summary + Display)) {
    println!("{x}: {}", x.summary());
}

fn show_summary2<T: Summary + Display>(x: &T) {
    println!("{x}: {}", x.summary());
}

5.3 As a return value

fn create() -> impl Summary {
    Tweet { user: "rust".into(), text: "1.0".into() }
}

impl Summary as a return type hides the concrete type, but it must be unique:

// fn create(short: bool) -> impl Summary {
//     if short { Tweet { .. } } else { Article { .. } }    // ERROR: two different types
// }

fn create_dyn(short: bool) -> Box<dyn Summary> {
    if short {
        Box::new(Tweet { user: "a".into(), text: "b".into() })
    } else {
        Box::new(Article { title: "t".into(), author: "a".into() })
    }
}

5.4 Calling an associated function on a generic

trait Zero {
    fn zero() -> Self;
}

impl Zero for u32 { fn zero() -> Self { 0 } }

fn fill<T: Zero>(n: usize) -> Vec<T> {
    (0..n).map(|_| T::zero()).collect()        // T::f(): associated function of the generic type
}

let v: Vec<u32> = fill(3);

6. The trait must be imported

mod shapes {
    pub trait Area { fn area(&self) -> f64; }
    impl Area for f64 { fn area(&self) -> f64 { self * self } }
}

fn main() {
    let x = 2.0_f64;
    // x.area();                 // ERROR: no method named `area` found
    use shapes::Area;            // the trait must be in scope
    x.area();
}

The methods of a trait are visible only if the trait is imported. This is intended: two crates can add an area method to f64 without conflict, as long as one file does not import both. Preludes (use runique::prelude::*, use std::io::prelude::*) import the common traits at once.


7. Deriving or implementing by hand

#[derive(Debug, Clone, PartialEq, Default)]
struct Point { x: i32, y: i32 }

#[derive(...)] generates the most obvious implementation, field by field. You write it by hand when the behavior you want is different:

TraitDerivedBy hand when...
Debugall fieldsa field is secret (password, token)
Cloneclones each fieldrarely
PartialEqcompares all fieldsequality is based on an identifier only
DefaultDefault of each fieldzero makes no sense (port, limit, delay)
PartialOrd/Ordfield orderthe order you want is not the field order
Hashall fieldsmust stay consistent with a hand-written PartialEq

Example: `Debug` without the secret

use std::fmt;

struct Credentials { user: String, password: String }

impl fmt::Debug for Credentials {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Credentials")
            .field("user", &self.user)
            .field("password", &"***")
            .finish()
    }
}

A #[derive(Debug)] would print the password in the logs at the first {:?} or the first dbg!.


8. The standard traits to know

TraitUsed forDerivable
Debug{:?}, dbg!, assert_eq!yes
Display{}, .to_string()no
Clone, Copycopyingyes
Defaultdefault valueyes
PartialEq, Eq==yes
PartialOrd, Ord<, sortingyes
HashHashMap keyyes
From, Intoconversion that cannot failno
TryFrom, TryIntoconversion that can failno
FromStr"..".parse::<T>()no
AsRef, Borrowborrowing in another formno
Iterator, IntoIteratoriterating, forno
Deref, Dropsmart pointersno
Add, Sub, Mul, Index...operators (see traits-avances)no
Errorerror typeno (thiserror)
Send, Syncthreads (automatic)—
Serialize, Deserialize (serde)serializationyes, with serde

9. Implementing `Display`

use std::fmt;

struct Price { cents: i64 }

impl fmt::Display for Price {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}.{:02} €", self.cents / 100, self.cents % 100)
    }
}

let p = Price { cents: 1250 };
println!("{p}");                 // 12.50 €
let text = p.to_string();        // provided by Display

Respecting the formatting options

struct Code(&'static str);

impl fmt::Display for Code {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.pad(self.0)               // pad applies the requested width and alignment
    }
}

println!("[{:>8}]", Code("abc"));  // [     abc]

write!(f, ...) ignores {:>8}; f.pad(...) respects it.


10. Conversions: `From`, `Into`, `TryFrom`, `FromStr`

10.1 `From`: you write one, you get the other

struct Celsius(f64);
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) }
}

let f = Fahrenheit::from(Celsius(100.0));
let f: Fahrenheit = Celsius(100.0).into();     // Into comes for free

You always implement From, never Into: the standard library provides Into automatically for every From.

10.2 `TryFrom`: a conversion that can fail

struct Age(u8);

impl TryFrom<i32> for Age {
    type Error = String;
    fn try_from(v: i32) -> Result<Self, Self::Error> {
        if (0..=150).contains(&v) {
            Ok(Age(v as u8))
        } else {
            Err(format!("impossible age: {v}"))
        }
    }
}

let a = Age::try_from(30);
let b: Result<Age, _> = 200_i32.try_into();   // the suffix says which conversion to use

10.3 `FromStr`: for `parse`

use std::str::FromStr;

#[derive(Debug)]
enum Color { Red, Green, Blue }

impl FromStr for Color {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.trim().to_lowercase().as_str() {
            "red" => Ok(Color::Red),
            "green" => Ok(Color::Green),
            "blue" => Ok(Color::Blue),
            other => Err(format!("unknown color: {other}")),
        }
    }
}

let c: Color = "Red".parse().unwrap();
let d = "purple".parse::<Color>();            // Err

FromStr (text → value) and Display (value → text) form a pair: a type that has both can be written out and read back.


11. Comparisons: `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`

11.1 Equality on an identifier

use std::hash::{Hash, Hasher};

struct User { id: u32, name: String }

impl PartialEq for User {
    fn eq(&self, other: &Self) -> bool { self.id == other.id }
}
impl Eq for User {}

impl Hash for User {
    fn hash<H: Hasher>(&self, h: &mut H) { self.id.hash(h); }   // consistent with eq
}

Mandatory rule: if a == b, then hash(a) == hash(b). A Hash that included name while eq does not compare it would silently break HashMaps.

11.2 `PartialEq` or `Eq`, `PartialOrd` or `Ord`

  • PartialEq: == exists, but some values may not equal themselves (f64::NAN != f64::NAN).
  • Eq: equality is total (a == a always). No method to write: it is a promise.
  • PartialOrd: some pairs are not comparable (NaN).
  • Ord: everything is comparable; required by sort(), BTreeMap, max().

11.3 A chosen order

use std::cmp::Ordering;

#[derive(PartialEq, Eq)]
struct Task { priority: u8, title: String }

impl Ord for Task {
    fn cmp(&self, other: &Self) -> Ordering {
        other.priority.cmp(&self.priority)                // priority descending
            .then_with(|| self.title.cmp(&other.title))    // then title ascending
    }
}

impl PartialOrd for Task {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}

PartialOrd delegates to Ord to stay consistent: this is the standard form.

Often, a one-off sort is enough without implementing Ord: v.sort_by_key(|t| std::cmp::Reverse(t.priority)).


12. Several traits on the same type

12.1 One type, several behaviors

use std::fmt;

#[derive(Debug, Clone, PartialEq)]
struct Temperature(f64);

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.1} °C", self.0) }
}

impl From<f64> for Temperature {
    fn from(v: f64) -> Self { Temperature(v) }
}

impl Default for Temperature {
    fn default() -> Self { Temperature(20.0) }
}

12.2 Two traits with a method of the same name

trait Pilot { fn fly(&self) -> &'static str; }
trait Wizard { fn fly(&self) -> &'static str; }

struct Human;

impl Pilot for Human { fn fly(&self) -> &'static str { "takeoff" } }
impl Wizard for Human { fn fly(&self) -> &'static str { "broom" } }
impl Human { fn fly(&self) -> &'static str { "flapping arms" } }

fn main() {
    let h = Human;
    h.fly();                         // the type's own method wins: "flapping arms"
    Pilot::fly(&h);                  // "takeoff"
    Wizard::fly(&h);                 // "broom"
    <Human as Pilot>::fly(&h);       // full form
}

For an associated function without self, only the full form works: <Human as Pilot>::name().


13. The coherence rule and extension traits

13.1 The orphan rule

You can write impl Trait for Type only if the trait or the type is defined in your crate.

use std::fmt;

struct MyType;
trait MyTrait {}

impl fmt::Display for MyType {                           // OK: the type is local
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "my type")
    }
}

impl MyTrait for Vec<i32> {}                             // OK: the trait is local

// impl fmt::Display for Vec<i32> { .. }                 // ERROR E0117: neither one

Why: if two crates could each implement Display for Vec<i32>, the compiler would not know which one to choose in a program that uses both.

13.2 Working around it: the newtype

use std::fmt;

struct List(Vec<i32>);

impl fmt::Display for List {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let texts: Vec<String> = self.0.iter().map(|x| x.to_string()).collect();
        write!(f, "[{}]", texts.join(" | "))
    }
}

13.3 Adding methods to a foreign type: the extension trait

trait TextExt {
    fn is_blank(&self) -> bool;
    fn truncate_chars(&self, n: usize) -> String;
}

impl TextExt for str {
    fn is_blank(&self) -> bool {
        self.trim().is_empty()
    }
    fn truncate_chars(&self, n: usize) -> String {
        self.chars().take(n).collect()
    }
}

fn main() {
    "   ".is_blank();                     // true
    "hello".truncate_chars(3);            // "hel"
    String::from("abc").truncate_chars(1); // also works on String (Deref to str)
}

The trait is local: the orphan rule is respected. Naming convention: TypeNameExt. itertools::Itertools adds dozens of methods to all iterators this way.


14. The same feature written in several ways

Computing the price including tax for several kinds of items.

// 1. One function per type: duplication
fn gross_book(price: f64) -> f64 { price * 1.055 }
fn gross_game(price: f64) -> f64 { price * 1.20 }

// 2. An enum: closed set, one match
enum Item { Book(f64), Game(f64) }
impl Item {
    fn gross(&self) -> f64 {
        match self {
            Item::Book(p) => p * 1.055,
            Item::Game(p) => p * 1.20,
        }
    }
}

// 3. A trait with an associated constant and a default method: open set
trait Taxable {
    const RATE: f64;
    fn net_price(&self) -> f64;
    fn gross(&self) -> f64 { self.net_price() * (1.0 + Self::RATE) }
}

struct Book(f64);
struct Game(f64);
impl Taxable for Book { const RATE: f64 = 0.055; fn net_price(&self) -> f64 { self.0 } }
impl Taxable for Game { const RATE: f64 = 0.20; fn net_price(&self) -> f64 { self.0 } }

fn total<T: Taxable>(items: &[T]) -> f64 {
    items.iter().map(|a| a.gross()).sum()
}
VersionAdding an item typeAdding an operation
functionsone more function per operationone function per type
enumchange the enum and every matchone more match
traitone more impl, elsewherechange the trait (with a default: without breaking)

Note: a trait with an associated constant cannot be used as dyn Taxable (see box-dynamiques); for mixed items in a Vec, you would replace RATE with a method fn rate(&self) -> f64.


15. Practical exercises

Exercise 1 — `Convertible` trait

Define Convertible with fn to_text(&self) -> String and a default method fn to_uppercase_text(&self) -> String. Implement it for i32 and for a struct Person { name: String }.

// Solution:
trait Convertible {
    fn to_text(&self) -> String;
    fn to_uppercase_text(&self) -> String {
        self.to_text().to_uppercase()
    }
}

impl Convertible for i32 {
    fn to_text(&self) -> String { self.to_string() }
}

struct Person { name: String }

impl Convertible for Person {
    fn to_text(&self) -> String { format!("Person({})", self.name) }
}

Exercise 2 — `FromStr` and `Display`

Write struct Version { major: u32, minor: u32 } that is read from "4.0" and printed as "4.0".

// Solution:
use std::fmt;
use std::str::FromStr;

struct Version { major: u32, minor: u32 }

impl FromStr for Version {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (a, b) = s.split_once('.').ok_or("expected format: X.Y")?;
        Ok(Version {
            major: a.parse().map_err(|_| "invalid major")?,
            minor: b.parse().map_err(|_| "invalid minor")?,
        })
    }
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}.{}", self.major, self.minor)
    }
}

let v: Version = "4.0".parse().unwrap();
assert_eq!(v.to_string(), "4.0");

ok_or("...")? and map_err(|_| "...")? work here because String: From<&str>: ? converts the &str into a String.

Exercise 3 — sorting by a chosen order

Sort a Vec<Version> from newest to oldest, without implementing Ord, then by implementing it.

// Solution 1: sort_by
fn sort_versions(v: &mut [Version]) {
    v.sort_by(|a, b| (b.major, b.minor).cmp(&(a.major, a.minor)));
}

// Solution 2: Ord (natural ascending order), then reverse
// #[derive(PartialEq, Eq, PartialOrd, Ord)] struct Version { major: u32, minor: u32 }
// v.sort(); v.reverse();

With derive, the order compares major then minor: the field order makes the sort order.


16. Cheat sheet

trait T {
    fn required(&self) -> u32;
    fn with_default(&self) -> u32 { self.required() * 2 }
    fn associated() -> Self where Self: Sized;
    const N: u32;
}
impl T for MyType { .. }

fn f(x: &impl T)              fn f<X: T>(x: &X)          fn f<X>(x: &X) where X: T
fn f(x: &dyn T)               fn f() -> impl T           fn f() -> Box<dyn T>
X::associated()               <MyType as T>::associated()  T::method(&x)

impl fmt::Display for X       impl From<A> for B         impl TryFrom<A> for B
impl FromStr for X            impl PartialEq for X       impl Ord + PartialOrd
trait StrExt { .. } impl StrExt for str { .. }            // extension trait

Key points

  • A trait declares a behavior; a type implements it, even after the fact.
  • Ask for few required methods, provide the rest by default.
  • The methods of a trait are only visible if the trait is imported.
  • Derive when the obvious behavior fits; write by hand for a secret, an equality on an identifier, a non-zero default.
  • Implement From, never Into. FromStr + Display for a type that can be written out and read back.
  • Hash must stay consistent with PartialEq.
  • Orphan rule: the trait or the type must be local; otherwise use a newtype or an extension trait.