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;
}
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
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"),
};
What can we do with it? Can we print a book?
println!("My book: {my_book}");
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`
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 {
...
}
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)]
|
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
}
}
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).
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.
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
💡 &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.
💡 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)]
|
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
}
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.
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
}
Unfortunately, it doesn’t compile:
error: cannot find derive macro `Display` in this scope
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)
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 },
}
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)
}
}
}
}
}
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 { ... }
}
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())
}
}
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)
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]
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.
<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.
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)
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
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.
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
}
}
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`
|
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
}
}
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)