Talking About Rust’s Traits

We previously talked about The Basics of Rust structs. That was some pretty cool stuff – we learned how to structure data and how to attach functionality to that data by using impl. A struct defines what data we have, but what if we want to define how that data should function?

Using the traits Rusting and Broken at the same time.

Enter Rust’s Traits

A trait lets us define how data will behave. Rust’s traits are similar to interfaces in other programming languages – it’s a guarantee of functionality that we’re going to provide. This makes programming significantly easier.

Declaring a trait is easy and looks a lot like a struct definition:

pub trait Debug {
    fn fmt(&self, &mut Formatter) -> Result;
}

This is Rust’s own Debug trait. I removed a little bit of the code from it just to make it readable, but you can look at the code for the Debug trait.

In this code, Rust we’re defining a public trait named Debug. Debug has one function named format that accepts a reference to the implementor and a mutable reference to a Formatter. The Formatter just tells Rust how to display whatever we’re debugging.

Debug is a bit of an odd choice since it’s normally used with a #derive annotation, but I really didn’t feel like coming up with some kind of pet or shape or other crazy example that didn’t rely on 900 lines of scaffolding to make it work.

Implementing a Trait

We implement traits the same way that we implement structs: using the impl keyword. Let’s implement the Debug trait for the Todo struct we looked at the last time.

use std::fmt;

pub struct Todo {
    pub title: String,
    pub description: String,
    created_at_s: i64,
    completed_at_s: Option
}

impl fmt::Debug for Todo {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "DO THIS:\n{{\n title: {}\n description: {}\n created: {}\n completed{:?}\n}} ",
               self.title,
               self.description,
               self.created_at_s)
    }
}

fn main() {
    let t = Todo {  title: "lol".to_string(),
                    description: "wut".to_string(), 
                    created_at_s: 0, 
                    completed_at_s: None };

    println!("{:?}", t);
}

// Output:
// DO THIS:
// {
//  title: lol
//  description: wut
//  created: 0
//  completed: None
// } 

It actually worked. That’s crazy!

What’s happening here?

In main, we create a Todo and then print it using the println! macro. The macro, here, has two arguments: a format string, and a thing to print. The format string is the "{:?}" thing. The curly braces mean we’re substituting something in and the bit inside the curly braces (the :? part) says “Use the Debug trait to dump this garbage to screen”.

When Rust evaluates println!, it goes off looking for a way to use the Debug trait on our Todo. If we hadn’t implemented that, the compiler would’ve complained long before the program could run and then everyone would be angry. Thankfully, we’ve implemented this trait by writing impl fmt::Debug for Todo – this tells the compiler that we’re implementing the Debugtrait (housed in fmt) for the Todo struct. Whenever we need to debug print a Todo, Rust now knows where to look for it.

You can play with this code over in the Rust playground.

Doing More with Traits

There’s a lot that’s possible with traits. We can add multiple traits to a single struct, you can see examples of this all over the Rust standard library. Creating multiple traits lets us define fine grained units of functionality and apply them wherever we need them. One example of this is both the Read and Write traits. These are used on files and streams, but they provide a sane way to interact with anything that is either readable or writable.

It’s also possible to extend other types with a trait. You don’t need to have defined the data type to be able to extend it with your own trait. It’s possible to add traits to primitive types like i32 or to complex types like a file or even a database connection.

There arew two rules for doing this yourself:

  1. You have to explicitly bring the trait you want into scope.
  2. You must have defined either the trait or the type that you’re messing with in the same library as the impl that you’re writing. This prevents you from hijacking the ToString trait on primitive types.

If you want to dig further into traits the Traits chapter of the Rust book is helpful and will lead you to further investigations into Rust’s traits.


Rusting” by ben dalton is licensed with CC BY-SA 2.0

 

2 Comments. Leave new

  • You can do so much more than that with traits, such as enhancing the primitive types with new traits.

    trait Digits {
        // Count the number of digits in a number
        fn digits(Self) -> usize;
    }
    impl Digits for usize {
        fn digits(&self) -> usize { ... }
    }
    

    There are a lot of useful Traits to implement as well, such as Default and Iterator.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Menu