DEV Community

Serokell
Serokell

Posted on • Originally published at serokell.io on

Get Started with Rust: Traits

Get Started with Rust: Traits

A trait is a basic language concept for defining shared behavior on types. Traits describe an interface that types can implement.

Rust traits are a sibling of Scala traits and Haskell type classes, as well as a cousin of C++ and Java interfaces.

This article will show you how to use traits in Rust. After reading it, you’ll be able to answer these questions:

  • What is a trait?
  • Why do we use traits in Rust?
  • How to implement and define traits in Rust?
  • What does it mean to derive a trait, and when can we do it?

We expect that you are somewhat familiar with structs and enums in Rust.

What is a trait?

Traits allow us to define interfaces or shared behaviors on types. To implement a trait for a type, we need to implement methods of that trait.

For example, let’s look at the simplified version of the PartialEq trait, which allows us to define equality for user-defined types:

trait PartialEq {
    fn eq(&self, other: &Self) -> bool;
}

Enter fullscreen mode Exit fullscreen mode

For a type to implement PartialEq, it needs to implement its method: eq. Implementing it allows us to write x == y and x != y for this type.

Traits are everywhere

You don’t need to dig too deep to find traits in Rust. Let’s look at a couple of examples.

You might be familiar with for loops:

for i in [1, 2, 3] {
    println!("{i}");
}
// Prints: 
// 1
// 2
// 3

Enter fullscreen mode Exit fullscreen mode

Rust’s for loop syntax is actually syntactic sugar for iterators, which are responsible for the logic of iterating over some items.

There’s a trait in the standard library for converting something into an iterator called IntoIterator. This is usually implemented for types that describe a collection. This includes Vectors, HashMaps, and Options.

Let’s look at another example.

Imagine that we’ve defined a simple Book struct and created an instance:

struct Book {
    title: String,
    author: String
}

let my_book = Book {
    title: String::from("Case of the Stuffed Goose"),
    author: String::from("Theo Witty"),
};

Enter fullscreen mode Exit fullscreen mode

What can we do with it? Can we print a book?

println!("My book: {my_book}");

Enter fullscreen mode Exit fullscreen mode

No. The code doesn’t compile:

error: `Book` doesn't implement `std::fmt::Display`
help: the trait `std::fmt::Display` is not implemented for `Book`

Enter fullscreen mode Exit fullscreen mode

And the compiler gives us a hint. std::fmt::Display is a trait that we must implement for our Book struct if we want to print it out.

What else can we try? Can we compare two books?

let your_book = Book {
    title: String::from("Case of the Shrieking Bacon"),
    author: String::from("Casey Fennimore"),
};

if my_book == your_book {
    ...
}

Enter fullscreen mode Exit fullscreen mode

Also no. We get a slightly different, but similar compilation error:

error: binary operation `==` cannot be applied to type `Book`
   |
   | my_book == your_book
   | ------- ^^ --------- Book
   | |
   | Book
   |
note: an implementation of `PartialEq<_>` might be missing for `Book`
help: consider annotating `Book` with `#[derive(PartialEq)]`
   |
   | #[derive(PartialEq)]
   |

Enter fullscreen mode Exit fullscreen mode

We must implement PartialEq for our Book struct if we want to check for equality using the == operator.

We’ve got a hint that we should provide an implementation for these traits. Let’s do that next.

How to implement a trait?

How to implement a trait manually?

We should keep in mind that a trait defines an interface. In order for a type to implement a trait, it must provide definitions of all the required methods.

Let’s implement the PartialEq trait for Book. The only method we have to define is eq.

impl PartialEq for Book {
    fn eq(&self, other: &Self) -> bool {
        self.title == other.title && self.author == other.author
    }
}

Enter fullscreen mode Exit fullscreen mode

These are a lot of new words at once, so let’s take a closer look. The first line defines the trait implementation:

//[1][2] [3] [4]
impl PartialEq for Book {

// [1]: `impl` keyword.
// [2]: The trait name (in this case, PartialEq).
// [3]: `for` keyword.
// [4]: The type name (in this case, Book).

Enter fullscreen mode Exit fullscreen mode

Then we have to define the method:

// [1][2] [3]
    fn eq(&self, other: &Self) -> bool {
// [4]
        self.title == other.title && self.author == other.author
    }
}

// [1]: Trait instance method has
// [2]: the **&self** parameter as the first parameter;
// [3]: any extra parameters must come after.
// [4]: Implementation details.

Enter fullscreen mode Exit fullscreen mode

Note: eq is an instance method – a trait method that requires an instance of the implementing type via the &self argument. Traits can also define static methods that don’t require an instance and do not have &self as their first parameter, but we aren’t going to cover them here.

The self is an instance of the implementing type that gives us access to its internals. For example, we can get our struct fields with self.title and self.author.

The actual implementation of eq is quite primitive. We compare each field between the structs and make sure that all of them are equal:

    self.title == other.title && self.author == other.author

Enter fullscreen mode Exit fullscreen mode

💡 &self is syntactic sugar for self: &Self.

The Self keyword is only available within type definitions, trait definitions, and impl blocks. In trait definitions, Self stands for the implementing type. For more information, see The Rust Reference.


We can compare our books now:

if my_book == your_book {
    println!("We have the same book!");
} else {
    println!("We have different books.");
}
// Prints: 
// We have different books.

Enter fullscreen mode Exit fullscreen mode

💡 Which common traits should my types implement?

There is no list of required traits. If you are writing an application, implement traits that you need as the need arises. If you are writing a library that will be used by others, it’s trickier. On the one hand, not adding a trait can limit your library users. On the other hand, removing a “wrong” trait is not backward-compatible. You can find more information on interoperability in the Rust API Guidelines.


Defining traits can be tedious and error-prone. That’s why it’s common to ask the compiler for help. The Rust compiler can provide an implementation for some of the traits via the derive mechanism.

Deriving a trait

Let’s drop the PartialEq implementation for the Book and rewind back to the compiler error:

error: binary operation `==` cannot be applied to type `Book`
   |
   | my_book == your_book
   | ------- ^^ --------- Book
   | |
   | Book
   |
note: an implementation of `PartialEq<_>` might be missing for `Book`
help: consider annotating `Book` with `#[derive(PartialEq)]`
   |
   | #[derive(PartialEq)]
   |

Enter fullscreen mode Exit fullscreen mode

The compiler not only tells us what the issue is but also suggests a solution for it. Let’s follow the instructions and annotate the Book struct with #[derive(PartialEq)]:

#[derive(PartialEq)]
struct Book {
    title: String,
    author: String
}

Enter fullscreen mode Exit fullscreen mode

We can compare the books again and it should behave the same:

if my_book == your_book {
    println!("We have the same book!");
} else {
    println!("We have different books.");
}
// Prints: 
// We have different books.

Enter fullscreen mode Exit fullscreen mode

derive generates the implementation of a trait for us. We don’t have to do the work ourselves. It provides a generally useful behavior that we don’t have to worry about. For example, in the case of derived PartialEq on structs, two instances are equal if all the fields are equal, and not equal if any fields aren’t equal.

We can derive multiple traits at the same time – derive accepts a list of all the required traits inside the parentheses. Let’s try to derive Display so we can print our books:

#[derive(PartialEq, Display)]
struct Book {
    title: String,
    author: String
}

Enter fullscreen mode Exit fullscreen mode

Unfortunately, it doesn’t compile:

error: cannot find derive macro `Display` in this scope

Enter fullscreen mode Exit fullscreen mode

It says that it can’t find the way to derive it in this scope. But the compiler won’t be able to find it in any scope! Because Display can’t be derived. The Display trait is used for user-facing output, and Rust cannot decide for you how to pretty print your type. Should it print the name of the struct? Should it print all the fields? You have to answer these and other questions yourself.

Deriving is limited to a certain number of traits. In case you want to implement a trait that can’t be derived or you want to implement a specific behavior for a derivable trait, you can write the functionality yourself.


💡 Which standard library traits can be derived?

Rust book’s Appendix C: Derivable Traits provides a reference of all the derivable traits in the standard library.

💡 Can the non-standard-library traits be derived?

Yes. Developers can implement derive for their own traits through procedural macros.

💡 When should I implement and when should I derive traits?

When deriving is available, it provides a general implementation of the trait, which is what you want most of the time. If it’s not the case, you can manually define the desired behavior.


How to define a trait?

Until now, we have only been talking about other people’s traits. But we can also define our own traits.

Imagine that we want to estimate reading time. It shouldn’t matter what type of content we are reading (an article, a novel, or even a poem). We could use the following trait:

// [1]
trait Estimatable {
// [2] [3]
    fn estimate(&self) -> f64;
}

// [1]: The trait name (in this case, Estimatable).
// [2]: Trait methods (in this case, only one).
// [3]: The **&self** parameter, followed by other parameters (in this case, none)

Enter fullscreen mode Exit fullscreen mode

Note: We are using a simple type for minutes for the sake of simplicity. Try not to do this at home! And try not to do this in production!

Any type implementing the Estimatable trait should define an estimate method that calculates how long it might take to read something in minutes.

Let’s try doing that.

Implementing a trait for an enum

In the previous section, we implemented a trait for a struct. Let’s try working with an enum this time. Imagine that we have a personal blog with different kinds of content:

enum Content {
    Infographic,
    PersonalEssay { text: String },
    TechArticle { text: String, topic: String },
}

Enter fullscreen mode Exit fullscreen mode

We can define the Estimatable trait for it. It’s not that different from implementing a trait for a struct, but note that we have to define behavior for all the enum variants:

impl Estimatable for Content {
    fn estimate(&self) -> f64 {
        match self {
            Content::Infographic => 1.0,

            Content::PersonalEssay { text } => 
                word_count(text) / AVG_WORDS_PER_MINUTE,

            // You have to pay attention, especially when learning Rust
            Content::TechArticle { text, topic } => {
                if topic == "Rust" {
                    word_count(text) / (0.5 * AVG_WORDS_PER_MINUTE)
                } else {
                    word_count(text) / (0.7 * AVG_WORDS_PER_MINUTE)
                }
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Note: For this example, imagine that word_count is a function that returns the number of words in a string and AVG_WORDS_PER_MINUTE is a constant for 250.0.

Default implementations

Let’s revisit the PartialEq trait. We saw a simplified version before, here is the actual definition:

pub trait PartialEq<Rhs = Self>
where
    Rhs: ?Sized,
{
    const fn eq(&self, other: &Rhs) -> bool;

    const fn ne(&self, other: &Rhs) -> bool { ... }
}

Enter fullscreen mode Exit fullscreen mode

When we defined the PartialEq implementation for the Book, we only had to implement one method, eq. But the definition clearly has two methods. How can this be?

Trait definitions can provide default method definitions. ne is a method with a default implementation.

As an example, we can extend the Estimatable trait with a display_estimate method:

trait Estimatable {
    fn estimate(&self) -> f64;

    fn display_estimate(&self) -> String {
        format!("{} minute(s)", self.estimate())
    }
}

Enter fullscreen mode Exit fullscreen mode

We don’t have to update the implementation for the Content type. It works as expected based on estimate:

println!("Estimated read: {}", Content::Infographic.display_estimate())
// Prints:
// Estimated read: 1 minute(s)

Enter fullscreen mode Exit fullscreen mode

Sometimes it’s useful to have default behaviors for the trait methods. When someone implements the trait for their type, they can decide whether to keep or override the default implementations.

Remember how we talked about iterators? The standard library provides a lot of useful methods for Iterator such as map, filter, and fold. As long as the collection implements either Iterator or IntoIterator, you get these for free.

(0..10) // A range of integers
  .filter(|x| x % 2 != 0) // Keep only odd numbers
  .map(|x| x * x) // Square each number
  .collect::<Vec<usize>>(); // Return a new Vector
// [1, 9, 25, 49, 81]

Enter fullscreen mode Exit fullscreen mode

How to use traits in function parameters?

Now that we know how to define our own traits, we can explore how to make use of them.

A trait is an interface that defines behaviors with a contract that other code can rely on. We can implement functions that depend on this interface.

In other words, we can implement functions that accept any type implementing our trait. The functions don’t need to know anything else about these types.

Let’s see how we could implement a function similar to println! that can print any Estimatable type:

// [1] [2] [3]  
fn summarize<T: Estimatable>(obj: T) {
    println!("Estimated read: {}", obj.display_estimate());
}

// [1]: The function takes any type T
// [2]: that is Estimatable.
// [3]: `obj` has a type T.

Enter fullscreen mode Exit fullscreen mode

<T: Estimatable> declares a generic type parameter with a trait bound. We can use trait bounds to restrict generics. In this case, it says that we accept any type T that implements the Estimatable trait. (If you would like to learn more about generic type parameters, see the Generic Data Types chapter of The Rust Book.)

In simple cases like this, we can use a more concise impl Trait syntax, which is syntactic sugar for trait bounds. We can rewrite it as follows:

// [1] [2] [3]
fn summarize(obj: impl Estimatable) {
    println!("Estimated read: {}", obj.display_estimate());
}

// [1]: The `obj` parameter should be a type
// [2]: that **impl** ements 
// [3]: the `Estimatable` trait.

Enter fullscreen mode Exit fullscreen mode

The syntax impl Trait is convenient in simple cases, while the trait bound syntax can express more complex use cases.

Either way, we can now call summarize:

summarize(Content::Infographic)
// Prints:
// Estimated read: 1 minute(s)

Enter fullscreen mode Exit fullscreen mode

If we try calling the summarize function with a type that doesn’t implement the Estimatable trait, the code won’t compile:

summarize(my_book)
// error: the trait bound `Book: Estimatable` is not satisfied

Enter fullscreen mode Exit fullscreen mode

With traits, we can build functions that accept any type as long as it implements a certain behavior.


💡 We can also use traits as return types from functions.

For example, you can return an iterator from a function. Using traits in return types is less common than in parameters. If you are interested, make sure to explore it in The Rust Book.


How to extend traits?

Rust doesn’t have a concept of inheritance. However, you can define supertraits to specify that a trait is an extension of another trait. For example, we can add another trait that adds a paywall to long reads on the website. For this, we need the ability to estimate from Estimatable, so we’ll extend it.

// [1] [2][3]
trait PayWallable : Estimatable {
    fn use_pay_wall(&self) -> bool;
}

// To define 
// [1]: a subtrait,
// [2]: that extends 
// [3]: the supertrait.

Enter fullscreen mode Exit fullscreen mode

If you want to implement a subtrait (PayWallable), you must implement all the required methods of the trait itself (PayWallable) as well as all the required methods of the supertrait (Estimatable). When you implement a method on a subtrait, you can use the functionality of the supertrait:

impl PayWallable for Book {
    fn use_pay_wall(&self) -> bool {
        self.estimate() > 10.0
    }
}

Enter fullscreen mode Exit fullscreen mode

But if you don’t implement the supertrait, you will get a compiler error:

error[]: the trait bound `Book: Estimatable` is not satisfied
    |
    | impl PayWallable for Book {
    | ^^^^^^^^^^^ the trait `Estimatable` is not implemented for `Book`
    |

Enter fullscreen mode Exit fullscreen mode

We can fix the error by implementing the supertrait as well:

impl Estimatable for Book {
    fn estimate(&self) -> f64 {
        let book_content = get_book_content_todo();
        word_count(&book_content) / AVG_WORDS_PER_MINUTE
    }
}

Enter fullscreen mode Exit fullscreen mode

Traits have many uses

In this blog post, we’ve covered one major use case for traits. Traits can be used as interfaces. Traits can serve as a contract that defines an interaction between components that use the interface. All types implementing a given trait must support the functionality defined by this trait, but it can be implemented differently for each type.

For example, a sort algorithm can be applied to a collection of items of any type, as long as they support comparison (implement the Ord trait).

But traits are more flexible than that. Look at what different things we can do with traits:

  • Traits can be used to extend the functionality of types. We can use traits to add methods to types that are defined in other libraries. For example, the itertools library adds a lot of convenience methods to all iterators (outside of their definition).

  • Traits can be used for dynamic dispatch (deciding which method to call at runtime). When used like this, they are very similar to Java interfaces.

There are many other uses: traits as abstract classes, as mix-ins, as behavioral markers, and for operator overloading.

Conclusion

Understanding traits and how the standard library traits work is an important part of learning Rust.

We have learned that traits in Rust are a way to add functionality to structs or enums and define shared behavior between different types. We’ve also explored how to use traits as interfaces and peeked at the various uses of traits.

If you would like to learn more about traits and the problems they solve, check out the Rust book chapters on traits and advanced traits, and the Rust Blog post about traits.

And if you would like to read more beginner-friendly articles about Rust, be sure to follow us on Twitter or subscribe to our newsletter via the form below.

Top comments (0)