kota's memex

Traits in rust are similar to interfaces in other languages, but not quite the same.

defining a trait

A trait defines the functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way.

A trait allows us to describe data not by what it is, but by what it can do.

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

default implementations

Rust allows defining a default implementation of a trait, which is useful in a few specific cases:

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

traits as parameters

We can use a trait as a parameter to a function or method and the caller will be able to provide any concrete type that implements the trait:

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

The syntax above is actually just a shorthand for the trait bound syntax which uses rust generics:

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

multiple trait bounds

There are a few different syntaxes to choose from when we want to specify that a concrete type must implement several traits.

pub fn notify(item: &(impl Summary + Display)) {
pub fn notify<T: Summary + Display>(item: &T) {
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{

conditionally implement methods

By using a trait bound with an impl block that uses generic type parameters, we can implement methods conditionally for types that implement the specified traits.

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

automatic implementation

Unfortunately, unlike go, we do not have traits implemented automatically as soon as a concrete type fulfills the interface. However, we can create traits which will become automatically implemented if other traits are implemented.

We can conditionally implement a trait for any type that implements another trait. The standard library implements the ToString trait on any type that implements the Display trait.

impl<T: Display> ToString for T {
    // --snip--
}

Because the standard library has this blanket implementation, we can call the to_string method defined by the ToString trait on any type that implements the Display trait.

// Integers implement the Display trait so they automatically implement
// ToString.
let s = 3.to_string();