Traits

In the previous session we implemented some functions on a struct. In Rust these functions can be grouped together to form a so-called trait. Like with humans, a trait is a combination of characteristics that all types that have that trait expose, let's say implement.

trait Fruitiness {
    fn is_sweet(&self) -> bool;
}

struct Pear {}

struct Lemon {}

impl Fruitiness for Pear {
    fn is_sweet(&self) -> bool {
        true
    }
}

impl Fruitiness for Lemon {
    fn is_sweet(&self) -> bool {
        false
    }
}

fn print_sweetness(id: &str, fruit: impl Fruitiness) {
    println!("{} is sweet? {}", id, fruit.is_sweet());
}

fn main() {
    let pear = Pear {};
    let lemon = Lemon {};
    print_sweetness("pear", pear);
    print_sweetness("lemon", lemon);
}

Traits can have default implementations that are available to all types that implement that trait. The default implementation can be overridden when desired.

trait Fruitiness {
    fn is_sweet(&self) -> bool {
        self.sweetness() >= 0.5
    }
    fn sweetness(&self) -> f32;
}

struct Pear {}

struct Lemon {}

impl Fruitiness for Pear {
    fn sweetness(&self) -> f32 {
        0.6
    }
}

impl Fruitiness for Lemon {
    fn sweetness(&self) -> f32 {
        0.2
    }
}

fn print_sweetness(id: &str, fruit: impl Fruitiness) {
    println!("{} is sweet? {}", id, fruit.is_sweet());
}

fn main() {
    let pear = Pear {};
    let lemon = Lemon {};
    print_sweetness("pear", pear);
    print_sweetness("lemon", lemon);
}

Traits are used throughout Rust, and often types implement a combination of traits, like the Display trait we discussed right at the beginning of this course.

use std::fmt::{Display, Formatter, Result};

trait Fruitiness {
    fn is_sweet(&self) -> bool {
        self.sweetness() >= 0.5
    }
    fn sweetness(&self) -> f32;
}

struct Pear {}

struct Lemon {}

impl Fruitiness for Pear {
    fn sweetness(&self) -> f32 {
        0.6
    }
}

impl Display for Pear {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        f.write_str("A pear")
    }
}

impl Fruitiness for Lemon {
    fn sweetness(&self) -> f32 {
        0.2
    }
}

impl Display for Lemon {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str("A lemon")
    }
}

fn print_sweetness(fruit: impl Fruitiness + Display) {
    println!("{} is sweet? {}", fruit, fruit.is_sweet());
}

fn main() {
    let pear = Pear {};
    let lemon = Lemon {};
    print_sweetness(pear);
    print_sweetness(lemon);
}

The signature of the Display trait is common to all objects in Rust. RustRover will actually scaffold the implementation for you the moment you type: impl Display for Pear {}. Use the alt + Enter keyboard combination while the cursor is on the (red underlined) line. Then pick "Implement members."

If you are more "mouse" oriented, you can also click on the red (or yellow) light bulb and pick the same option from the list.

Notice the "use" statements at the top of our code. This is Rust's way of importing external objects and traits into our application. Often these are automatically added by the IDE. You can use the same alt + Enter combination to add missing imports.

You can see that traits can be combined using the + operator. If you need a combination of many traits you can use a type signature to make you code more readable.

use std::fmt::{Debug, Display, Formatter, Result};

trait Fruitiness {
    fn is_sweet(&self) -> bool {
        self.sweetness() >= 0.5
    }
    fn sweetness(&self) -> f32;
}

struct Pear {}

struct Lemon {}

impl Fruitiness for Pear {
    fn sweetness(&self) -> f32 {
        0.6
    }
}

impl Display for Pear {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        f.write_str("A pear")
    }
}

impl Debug for Pear {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        f.write_str("A debugged pear")
    }
}

impl Fruitiness for Lemon {
    fn sweetness(&self) -> f32 {
        0.2
    }
}

impl Display for Lemon {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str("A lemon")
    }
}

impl Debug for Lemon {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        f.write_str("A debugged lemon")
    }
}

fn print_sweetness<T>(fruit: T)
    where
        T: Fruitiness + Display + Debug,
{
    println!("{} is sweet? {}", fruit, fruit.is_sweet());
}

fn main() {
    let pear = Pear {};
    let lemon = Lemon {};
    print_sweetness(pear);
    print_sweetness(lemon);
}

Without going into detail right now on how the mechanism actually works, I do want to point out that common traits can often be derived automatically by Rust. For example, the Debug trait in the above code, can be replaced with a derived implementation. It saves a few lines of code!

use std::fmt::{Debug, Display, Formatter, Result};

trait Fruitiness {
    fn is_sweet(&self) -> bool {
        self.sweetness() >= 0.5
    }
    fn sweetness(&self) -> f32;
}

#[derive(Debug)]
struct Pear {}

#[derive(Debug)]
struct Lemon {}

impl Fruitiness for Pear {
    fn sweetness(&self) -> f32 {
        0.6
    }
}

impl Display for Pear {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        f.write_str("A pear")
    }
}

impl Fruitiness for Lemon {
    fn sweetness(&self) -> f32 {
        0.2
    }
}

impl Display for Lemon {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str("A lemon")
    }
}

fn print_sweetness<T>(fruit: T)
    where
        T: Fruitiness + Display + Debug,
{
    println!("{:?} is sweet? {}", fruit, fruit.is_sweet());
}

fn main() {
    let pear = Pear {};
    let lemon = Lemon {};
    print_sweetness(pear);
    print_sweetness(lemon);
}

Reference material

Traits: Defining Shared Behavior