The best TRAIT of RUST 🔥 (no pun intended)

Liam Clegg - May 5 '23 - - Dev Community

Introduction

So, your friend can't stop about how they use Rust. Perhaps they've mentioned how it's safe, has great memory management or how its BLAZINGLY fast. Although this is clearly true, there is another great reason to use rust: its use of traits.

Never heard of traits? well its your lucky day.

What is a trait?

Do you remember when you first learnt inheritance and it seamed like this amazing idea. So you create an animal class that allows for a noise method and an eat method. You then you create a cat and a dog who have these methods. You then want a robot who speaks but doesn't eat. Well now you need a speaker class that the animal class extends and the robot class extends. Suddenly, when you realize that the cat and the robot both need to catch mice, you find yourself struggling to figure out what to do. Everything you were promised about inheritance seems like a lie.

Fortunately, there is another approach to this problem called composition. Instead of a cat being an animal, a cat is an entity composed of the traits speaker, eater, and mouse hunter. The robot is composed of the speaker and mouse hunter traits. With composition, you can create more flexible and reusable code that adapts to different contexts. This is why traits are super useful in rust.

Now, you are probably thinking that this already exists in other languages like java and c#. But there are a few nifty tricks that traits have in rust that do not exist in these other languages. We will see them a bit later.

The Vector Example

An example that I personally think shows off traits the most is vectors. The math kind not the array kind.

All code examples can be found on Github: rust-traits-example

Creating a Vec2 Struct

Lets first define a Vec2 struct with a generic type for the x and y attributes.

struct Vec2<T> {
    x: T,
    y: T
}
Enter fullscreen mode Exit fullscreen mode

Printing

Now in our main lets create a Vec2 and print it.

fn main() {
    let v1 = Vec2 {
        x: 1,
        y: 2,
    };

    println!("{}", v1);
}
Enter fullscreen mode Exit fullscreen mode

You will quickly realize this doesn't work when the compiler throws the error doesn't implement 'std::fmt::Display'. This happens because the Display trait must be implemented for the type that you are printing.
A quick solution to this will be to print using the Debug trait instead. This is because unlike the Display trait you can derive the Debug trait. This essentially a macro (or code generated at compile time) that will automatically implement the trait for you. This can be done by adding #[derive(Debug, Copy, Clone)] above the struct. Note: "{:?}" is used for debug in the print macro.

#[derive(Debug)]
struct Vec2<T> {
    x: T,
    y: T
}

fn main() {
    let v1 = Vec2 {
        x: 1,
        y: 2,
    };

    println!("{:?}", v1);
}
Enter fullscreen mode Exit fullscreen mode

Implementing traits for existing types

So now lets say we want to be able to convert an i32 (32 bit integer) to a Vec2. One way we can do this is by creating a ToVec2 trait that must derive a to_vec2 function.

trait ToVec2<T> {
    fn to_vec2(&self) -> Vec2<T>;
}

impl ToVec2<i32> for i32 {
    fn to_vec2(&self) -> Vec2<i32> {
        Vec2 { 
            x: *self,
            y: *self 
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This now means we can call the method to_vec2 on any variable with the i32 type. This is something that interfaces cannot do in other languages and why traits are so powerful in rust.
We can now modify the main function to show this working in action.

fn main() {
    let v1 = Vec2 {
        x: 1,
        y: 2,
    };

    let v2 = 8.to_vec2();

    println!("{:?}, {:?}", v1, v2);
}
Enter fullscreen mode Exit fullscreen mode

Operation overloading

Another use case for traits is operation overloading. This is where you have a struct or class and wish to allow the user to use an operator such as '+' to add the two values together.
Lets now implement the Add trait for Vec2. To do this we will first need to insure that the generic used for x and y also have this trait so they can be added to the other vectors x and y values. There are 2 types of syntax we can use for this.
The first way is to add a bound in the angle brackets. This is good for when your adding one or two traits.

use std::ops::Add;

#[derive(Debug)]
struct Vec2<T: Add> {
    x: T,
    y: T
}
Enter fullscreen mode Exit fullscreen mode

But lets say we wish to have multiple traits. In this case lets add the Copy and Clone traits too. We can now use the where keyword as follows.

use std::ops::Add;

#[derive(Debug)]
struct Vec2<T> 
where
    T: Add
       + Clone
       + Copy
{
    x: T,
    y: T
}
Enter fullscreen mode Exit fullscreen mode

We need to also make sure that these bounds exist on the ToVec2 trait since it also uses this generic.

trait ToVec2<T>
where
    T: Add
       + Clone
       + Copy
{
    fn to_vec2(&self) -> Vec2<T>;
}
Enter fullscreen mode Exit fullscreen mode

Now we need to implement Add for Vec2. To do this we will need to define the output type of type Vec2 and also we need to make sure the Add bound for T is Add since The output of the addition must also have the traits required for a Vec2.

impl<T> Add for Vec2<T>
where
    T: Add<Output = T>
       + Clone
       + Copy
{
    type Output = Vec2<T>;

    fn add(self, rhs: Self) -> Self::Output {
        Vec2 {
            x: self.x + rhs.x, 
            y: self.y + rhs.y
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Before we test this me need to make sure that Vec2 also derives the Copy and Clone to prevent issues related to borrowing.

#[derive(Debug, Clone, Copy)]
struct Vec2<T> 
Enter fullscreen mode Exit fullscreen mode

Finally we can test if the addition works by updating our main function.

fn main() {
    let v1 = Vec2 {
        x: 1,
        y: 2,
    };

    let v2 = 8.to_vec2();

    let v3 = v1 + v2;

    println!("{:?} + {:?} = {:?}", v1, v2, v3);
}
Enter fullscreen mode Exit fullscreen mode

IT WORKS!!!

https://github.com/cleggacus/rust-traits-example/tree/1_best_trait_of_rust

Challenge

Try and implement more operations for this struct and implement the ToVec2 trait for more types. If you would like to do your own further reading maybe try to replace the ToVec2 for the Into and From traits.

Conclusion

We've explored the issues with inheritance and how trait based composition can help this by using rust. We've looked at how traits are derived, used for operation overloading and implemented for already existing types. Hopefully this is a compelling reason to use fast apart from its BLAZINGLY FAST performance.

Thanks for reading!

P.S. If you share, comment and like this post I will show how we can one up this Vec2 struct with macros to do the work for us.

. . . . .