Follow

Follow

My Rust journey: Traits

Henry Barreto's photo
Henry Barreto
·Dec 13, 2020·

7 min read

Struct

To begin I guess interest to mention Structs due to the uses with Traits, but it also needed to clarify that it is possible to use it with others datatype like Enum.

In the book The Rust Programming Language has a definition of what I think that is wonderful: "A struct, or structure, is a custom data type that lets you name and package together multiple related values that make up a meaningful group". Basically, its data type what contains another data types inside it.

struct Person {
    name: String,
    age: u8,
}

This is a variation of structs uses, but it is enough to exemplify what is it. If compared with "normal" OO programming language, it is like that object's data while Traits are like behaviors.

There was no deepness here because the main topic it is not Structs it was just to show that Struct is important to Traits understatement, but if you want to know about it, Rust by Example and The Rust Programming Language can help you so much.

Traits

"A trait is a collection of methods defined for an unknown type: Self. They can access other methods declared in the same trait."

Knowing I'm trying to compare Rust with traditional OO programming language, Simplifying too, is enough to say that Traits are a way to share abstract behaviors to datatype. It is like a method and "abstract methods" in OO programming language. While Structs are that data, Traits are the methods.

On my search I found many variants of that phrase: "Rust Traits are like interfaces of others programming languages with some particulate characteristics", what make me have a better understanding of the theme, so I think it is worth to say here again.

Here is a self explained example from Rust By Example book.

struct Sheep { naked: bool, name: &'static str }

trait Animal {
    // Static method signature; `Self` refers to the implementor type.
        // That will be that "object's constructor"
    fn new(name: &'static str) -> Self;

    // Instance method signatures; these will return a string.
    fn name(&self) -> &'static str;
    fn noise(&self) -> &'static str;

    // Traits can provide default method definitions.
    fn talk(&self) {
        println!("{} says {}", self.name(), self.noise());
    }
}

// Note what Sheep impl has the same name as Sheep struct and had the basic
// methods for every Sheep
impl Sheep {
    fn is_naked(&self) -> bool {
        self.naked
    }

    fn shear(&mut self) {
        if self.is_naked() {
            // Implementor methods can use the implementor's trait methods.
            println!("{} is already naked...", self.name());
        } else {
            println!("{} gets a haircut!", self.name);

            self.naked = true;
        }
    }
}

// Implement the `Animal` trait for `Sheep`.
impl Animal for Sheep {
    // `Self` is the implementor type: `Sheep`.
    fn new(name: &'static str) -> Sheep {
        Sheep { name: name, naked: false }
    }

    fn name(&self) -> &'static str {
        self.name
    }

    fn noise(&self) -> &'static str {
        if self.is_naked() {
            "baaaaah?"
        } else {
            "baaaaah!"
        }
    }

    // Default trait methods can be overridden.
    fn talk(&self) {
        // For example, we can add some quiet contemplation.
        println!("{} pauses briefly... {}", self.name, self.noise());
    }
}

fn main() {
    // Type annotation is necessary in this case.
    let mut dolly: Sheep = Animal::new("Dolly");
    // TODO ^ Try removing the type annotations.

    dolly.talk();
    dolly.shear();
    dolly.talk();
}

Self

The Self keyword, in this case, is a parameter what contains a "reference" to access either the own trait definition or impl blocks. The & character found here has relation with the Rust memory management system (ownership) and deceive a full blog to it.

It is easy to get it through this piece of code:

struct Sheep { naked: bool, name: &'static str }

// This is a impl block to the Sheep struct
impl Sheep {
// Here is possible acess the data of a Sheep structure definition like
// properties of a class
    fn is_naked(&self) -> bool {
        self.naked
    }
        // This is a mutable reference, what means that 'self' can modify the
        // data from the struct Sheep
    fn shear(&mut self) {
        if self.is_naked() {
            // Implementor methods can use the implementor's trait methods.
            println!("{} is already naked...", self.name());
        } else {
            println!("{} gets a haircut!", self.name);
    // What have made here
            self.naked = true;
        }
    }
}

In the Animal Trait definition and implementation to Sheep has a different thing, the method new does not have the Self reference like others. It is means that is a Static Method and can be used without a Sheep definition. Normally, the new is like a constructor, a special method used to define a new struct with basic data.

// Implement the `Animal` trait for `Sheep`.
impl Animal for Sheep {
    // `Self` is the implementor type: `Sheep`.
        // This one does not use the self, but define and return a Sheep
    fn new(name: &'static str) -> Sheep {
        Sheep { name: name, naked: false }
    }

    fn name(&self) -> &'static str {
        self.name
    }

    fn noise(&self) -> &'static str {
        if self.is_naked() {
            "baaaaah?"
        } else {
            "baaaaah!"
        }
    }

    // Default trait methods can be overridden.
    fn talk(&self) {
        // For example, we can add some quiet contemplation.
        println!("{} pauses briefly... {}", self.name, self.noise());
    }
}

Attribute

The Rust compile provide some default Traits through the standard library. It does that via the #[derive] attribute added to a datatype. If a more complex behavior is needed, it is possible to add a custom implementation to that Traits.

From Rust by Example

  • Comparison traits: Eq, PartialEq, Ord, PartialOrd.
  • Clone, to create T from &T via a copy.
  • Copy, to give a type 'copy semantics' instead of 'move semantics'.
  • Hash, to compute a hash from &T.
  • Default, to create an empty instance of a data type.
  • Debug, to format a value using the {:?} formatter.
// `Centimeters`, a tuple struct that can be compared
#[derive(PartialEq, PartialOrd)]
struct Centimeters(f64);

// `Inches`, a tuple struct that can be printed
#[derive(Debug)]
struct Inches(i32);

impl Inches {
    fn to_centimeters(&self) -> Centimeters {
        let &Inches(inches) = self;

        Centimeters(inches as f64 * 2.54)
    }
}

// `Seconds`, a tuple struct with no additional attributes
struct Seconds(i32);

fn main() {
    let _one_second = Seconds(1);

    // Error: `Seconds` can't be printed; it doesn't implement the `Debug` trait
    //println!("One second looks like: {:?}", _one_second);
    // TODO ^ Try uncommenting this line

    // Error: `Seconds` can't be compared; it doesn't implement the `PartialEq` trait
    //let _this_is_true = (_one_second == _one_second);
    // TODO ^ Try uncommenting this line

    let foot = Inches(12);

    println!("One foot equals {:?}", foot);

    let meter = Centimeters(100.0);

    let cmp =
        if foot.to_centimeters() < meter {
            "smaller"
        } else {
            "bigger"
        };

    println!("One foot is {} than one meter.", cmp);
}

Return Traits

A Trait can be returned for a function. Indeed, a datatype which implement that Trait can be returned. The impl keyword is added to the function's return type with the Trait.

struct Programmer {}
struct Engineer {}
struct Doctor {}

trait Person {
    // default speak method
    fn speak(&self) -> String {
        format!("The person is speaking...")
    }
}

impl Person for Programmer {
    fn speak(&self) -> String {
        format!("There is a bug here...")
    }
}
impl Person for Engineer {
    fn speak(&self) -> String {
        format!("The bridge is ready")
    }
}
impl Person for Doctor {
    fn speak(&self) -> String {
        format!("Amoxicilina")
    }
}

// These functions return a Struct what implement Person Trait
fn create_a_programmer() -> impl Person {
    Programmer {}
}
fn create_an_engineer() -> impl Person {
    Engineer {}
}
fn create_a_doctor() -> impl Person {
    Doctor {}
}

fn main() {
    let programmer = create_a_programmer();
    let enginner = create_an_engineer();
    let doctor = create_a_doctor();

    println!("{}", programmer.speak());
    println!("{}", enginner.speak());
    println!("{}", doctor.speak());

    ()
}

"Inheritance"

In another analogy with traditional OO programming language and the core concepts of this, "inheritance" or supertraits comes to join different Traits together using the + signal. Other keyword in this code is dyn, what has relation with the Rust memory management, what I guess I will Blog about as soon as possible.

Good explanations: Dyn and dyn , impl and Trait Objects — Rust

trait Person {
    fn name(&self) -> String;
}

// Person is a supertrait of Student.
// Implementing Student requires you to also impl Person.
trait Student: Person {
    fn university(&self) -> String;
}

trait Programmer {
    fn fav_language(&self) -> String;
}

// CompSciStudent (computer science student) is a subtrait of both Programmer 
// and Student. Implementing CompSciStudent requires you to impl both supertraits.
trait CompSciStudent: Programmer + Student {
    fn git_username(&self) -> String;
}

fn comp_sci_student_greeting(student: &dyn CompSciStudent) -> String {
    format!(
        "My name is {} and I attend {}. My favorite language is {}. My Git username is {}",
        student.name(),
        student.university(),
        student.fav_language(),
        student.git_username()
    )
}

fn main() {}

That have been the main topics about Traits what I think it is worth trying to simplify and show how I think about this in my learning path. Of course, it does not have so deepness because I am limiting my learning scope to it does not drown me on the complex Rust world and gradually improve myself until a comfortable confident level.

Feel free to coment, correct me or just say a hi. Thanks for reading and I hope that help someone.

Did you find this article valuable?

Support Henry Barreto by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this