Module 11: Traits, Derive, and Generics
Lecture 26: Friday, April 3, 2026, and
Lecture 28: Wednesday, April 8, 2026.
In this module, we will learn about:
- How we can use generics to reduce code duplication.
- How to define shared behavior among many types using traits.
- How to combine traits and generics using trait bounds to write general reusable code!
- Learn about popular builtin traits and how we can implement them for our types using derive.
Example Scenario: Library
Let’s start with the following example that we will re-use throughout these notes. In this scenario, we are a public library that users check out books from.
Our starting point is defining what a book is. This is a new type! We can use struct to define it.
In this scenario, let’s say a book is made out of a title and an author name, both strings, and an ISBN and an edition
number, both integers.
pub struct Book {
pub title: String,
pub author: String,
pub isbn: u64,
pub edition: u64,
}
In this case, we can represent our library’s stock as a vector containing many books, perhaps including several copies of the same book! As an example, let’s say our library contains three copies of various editions of the first Harry Potter book, and one copy of the Art of Computer Programming.
let library_books: Vec<Book> = vec![
Book {
title: String::from("Harry Potter and The Philosopher's Stone"),
author: String::from("JK Rowling"),
isbn: 10,
edition: 1,
},
Book {
title: String::from("Harry Potter and The Philosopher's Stone"),
author: String::from("JK Rowling"),
isbn: 10,
edition: 1,
},
Book {
title: String::from("Harry Potter and The Philosopher's Stone"),
author: String::from("JK Rowling"),
isbn: 10,
edition: 2,
},
Book {
title: String::from("The Art of Computer Programming"),
author: String::from("Donald Knuth"),
isbn: 33,
edition: 1,
},
];
Now, say a user walks into our library looking for some book, say the first Harry Potter book, and wanting to borrow it. We will need to check our stock to find out whether we have any available copies of that book. Let’s build a function that does that.
// `book` is the book the user is looking for.
// `library_books` is the vector of books we have available.
// the function should return how many matching copies we have in our library.
fn available_copies(book: &Book, library_books: &Vec<Book>) -> u64 {
let mut count = 0;
for b in library_books {
// check if `b`, the book we are currently looking at, matches
// the requested book.
if b.title == book.title
&& b.author == book.author
&& b.isbn == book.isbn
&& b.edition == book.edition {
count += 1;
}
}
return count;
}
Let’s put all this together and test our code. Run the code below and observe its output!
pub struct Book { pub title: String, pub author: String, pub isbn: u64, pub edition: u64, } // `book` is the book the user is looking for. // `library_books` is the vector of books we have available. // the function should return how many matching copies we have in our library. fn available_copies(book: &Book, library_books: &Vec<Book>) -> u64 { let mut count = 0; for b in library_books { // check if `b`, the book we are currently looking at, matches // the requested book. if b.title == book.title && b.author == book.author && b.isbn == book.isbn && b.edition == book.edition { count += 1; } } return count; } fn main() { let library_books: Vec<Book> = vec![ Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 1, }, Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 1, }, Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 2, }, Book { title: String::from("The Art of Computer Programming"), author: String::from("Donald Knuth"), isbn: 33, edition: 1, }, ]; let target_book = Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 1 }; let count = available_copies(&target_book, &library_books); println!("The library has {} copies", count); }
Our First Trait: PartialEq
One downside of our code is how we implemented equality checking between books (the if statement inside available_copies).
The check is too verbose: it compares every field inside the two books to each other. This is not ideal: imagine if the
programmers had to change the Book type, by adding or removing some fields from it (e.g., to support having multiple authors for one book).
The programmers would then need to remember to change the code inside available_copies to match their changes to Book.
It also looks unnatural.
Perhaps a better approach is to replace that if statement with something more natural. For example, if b == book { ... }!
Modify the code above to use a direct equality check, run it, and look at the output!
You will find that Rust gives the following compile-time error:
error[E0369]: binary operation `==` cannot be applied to type `&Book`
--> src/main.rs:16:10
|
16 | if b == book {
| - ^^ ---- &Book
| |
| &Book
|
note: an implementation of `PartialEq` might be missing for `Book`
--> src/main.rs:1:1
Rust essentially complains about the equality check with ==. Specifically, Rust tells us that it does not
know what equality means for books: we defined the type Book ourselves, but did not tell Rust how to compare two books!
A good start would be to implement our own equality check function for Book. For example:
impl Book {
pub fn check_equals(&self, other_book: &Book) -> bool {
return self.title == other_book.title
&& self.author == other_book.author
&& self.isbn == other_book.isbn
&& self.edition == other_book.edition;
}
}
Now, we can call this function to check if two books are equal, e.g., using if b.check_equals(book) { ... }.
Here’s the complete code below, run it and observe the output!
pub struct Book { pub title: String, pub author: String, pub isbn: u64, pub edition: u64, } impl Book { pub fn check_equals(&self, other_book: &Book) -> bool { return self.title == other_book.title && self.author == other_book.author && self.isbn == other_book.isbn && self.edition == other_book.edition; } } // `book` is the book the user is looking for. // `library_books` is the vector of books we have available. // the function should return how many matching copies we have in our library. fn available_copies(book: &Book, library_books: &Vec<Book>) -> u64 { let mut count = 0; for b in library_books { // check if `b`, the book we are currently looking at, matches // the requested book. if b.check_equals(book) { count += 1; } } return count; } fn main() { let library_books: Vec<Book> = vec![ Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 1, }, Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 1, }, Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 2, }, Book { title: String::from("The Art of Computer Programming"), author: String::from("Donald Knuth"), isbn: 33, edition: 1, }, ]; let target_book = Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 1 }; let count = available_copies(&target_book, &library_books); println!("The library has {} copies", count); }
This is better, but it is explicitly calling check_equals. It is more natural to use ==. Modify the above code to use == and try to run it again.
error[E0369]: binary operation `==` cannot be applied to type `&Book`
--> src/main.rs:26:10
|
26 | if b == book {
| - ^^ ---- &Book
| |
| &Book
|
note: an implementation of `PartialEq` might be missing for `Book`
--> src/main.rs:1:1
The same error!
Rust is unaware that check_equals correspond to ==. Why would it?! Rust does not understand English, and has no way
of knowing that we intended for check_equals to define how to do ==.
How can we inform Rust of this intention? We can using traits!
Specifically, Rust provides a trait called PartialEq, which corresponds to the == operation. We can implement it for Book instead.
impl PartialEq for Book {
fn eq(&self, other_book: &Book) -> bool {
return self.title == other_book.title
&& self.author == other_book.author
&& self.isbn == other_book.isbn
&& self.edition == other_book.edition;
}
}
Now, we can put all this together. Look at what the if condition now checks in available_copies. Run the code to confirm it works.
pub struct Book { pub title: String, pub author: String, pub isbn: u64, pub edition: u64, } impl PartialEq for Book { fn eq(&self, other_book: &Book) -> bool { return self.title == other_book.title && self.author == other_book.author && self.isbn == other_book.isbn && self.edition == other_book.edition; } } // `book` is the book the user is looking for. // `library_books` is the vector of books we have available. // the function should return how many matching copies we have in our library. fn available_copies(book: &Book, library_books: &Vec<Book>) -> u64 { let mut count = 0; for b in library_books { if b == book { count += 1; } } return count; } fn main() { let library_books: Vec<Book> = vec![ Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 1, }, Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 1, }, Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 2, }, Book { title: String::from("The Art of Computer Programming"), author: String::from("Donald Knuth"), isbn: 33, edition: 1, }, ]; let target_book = Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 1 }; let count = available_copies(&target_book, &library_books); println!("The library has {} copies", count); }
Traits
Traits are Rust’s way of defining a contract. Think of it as Rust’s way of declaring that some types implement certain behavior.
For example, the PartialEq trait defines a contract for equality checking. Specifically, instances of any type that implements PartialEq
can be compared against each other using ==. Another example is the Clone trait: instances of types that implement Clone can be cloned using .clone()!
Thinking about traits involves two facets:
- What is the trait definition or contract?
- What types implement this trait (and how)?
Let’s start by looking at the definition of PartialEq. We put a simplified version of its implementation
below. If you are curious, you can look at its docs for more details.
pub trait PartialEq {
// Required method that we must implement for our types
// when we implement PartialEq for them.
// Self: our type that we are implementing the trait for (for example, Book).
fn eq(&self, other: &Self) -> bool;
}
We highlight the following:
- We define a trait using the
traitkeyword and then giving the trait a name, similar to howstructorfnworks. - Inside the trait, we can define one or more function signatures. These are the behaviors required by the trait contract.
- Every type that implements this trait must implement all these functions.
- The trait itself does not specify how these functions are implemented – notice how they have no bodies!
- Instead, when we implement a trait for some type, that’s when we have to provide the function body and implementation.
For example, this is how we implemented PartialEq for Book above:
impl PartialEq for Book {
fn eq(&self, other_book: &Book) -> bool {
return self.title == other_book.title
&& self.author == other_book.author
&& self.isbn == other_book.isbn
&& self.edition == other_book.edition;
}
}
We highlight the following observations:
- The syntax for implementing a trait is
impl <trait name> for <type name>. - Inside the
implblock, you need to provide an implementation for every function that the trait defines.
Specifically, if you make a typo in the name of the function (say equals instead of eq), or if you mess up the
signature, the compiler will give you an error.
You can read more about traits in the Rust book.
Other Builtin Traits
In addition to PartialEq, Rust has a number of other commonly used builtin traits:
- Clone: indicates that instances of a type can be cloned.
- Debug: indicates that instances of a type can be printed using
println!("{:?}", <instance>);. - Display: indicates that instances of a type can be printed using
println!("{}", <instance>);. - PartialOrd: indicates that instances of a type can be compared to each other using
<,>, etc.
Furthermore, there are several common and widely used traits that are provided by common libraries in Rust. We have seen some of them already in project 3!
- Serialize: indicates that instances of a type can be serialized, i.e., transformed to binary or to JSON.
- Deserialize: indicates that instances of a type can be deserialized, i.e., retrieved from binary or from JSON.
Derive
When we define a custom type, like Book, it is common to implement many of these traits for that type (when they make sense).
In many cases, this implementation is not interesting: to implement PartialEq, we frequently simply want to compare all matching fields from two objects.
When implementing Clone, we often simply want to clone every field.
In these cases, Rust allows us to use derive to automatically implement these traits for our types, without writing out all the code and implementation.
For example, instead of impl PartialEq for Book {...} block, we can instead just write the following:
#[derive(PartialEq)]
pub struct Book {
pub title: String,
pub author: String,
pub isbn: u64,
pub edition: u64,
}
You can derive more than one trait by separating them with a comma. For example #[derive(PartialEq, Clone)].
Exercise: modify the scenario code above to use derive instead of manual implementation of the trait. Run the code to confirm it works.
When should you use derive?
Use derive for builtin or external traits where you want to implement them the “default” way, as in, by applying the trait to a type’s fields or components.
When should you not use derive?
derive is not available for all traits. If you define your own custom trait, it will not be derivable by default. You will need to implement your own
derive macro for it to enable automatic deriving.
Furthermore, even if a trait support derive, you may still want to manually implement it if you want to specify custom, non-default logic for how to implement
its behavior.
For example, the default PartialEq implementation compares all the fields in the two objects to each other. However, let’s say that we want to consider books with the same
ISBN to be equal regardless of edition. This way, if the only available copy of the Harry Potter book has edition 2, the user can still find it and check it out!
// This is a non-default implementation of PartialEq.
// If this is our goal, then we cannot use derive.
impl PartialEq for Book {
fn eq(&self, other_book: &Book) -> bool {
return self.isbn == other_book.isbn;
}
}
Exercise: modify the scenario code above to compare books based on ISBN only. Run the code. How many available copies of the first Harry Potter book get identified after your modification?
Using Generics to Avoid Code Duplication
Our available_copies function seems like a helpful and reusable helper function: it counts how many matching elements a vector contains. You could
imagine using it for all sorts of examples, not just a library with books! However, the way we implemented this function is very specific:
it only works for a Book and a vector of Books. It would not work for, for example, a String with a vector of Strings!
Let’s see if we can change that using generics. Rather than defining this function for the specific Book type (this is often called a concrete type),
we can define it for a generic/general type T as follows:
#![allow(unused)] fn main() { fn count_occurrences<T>(element: &T, vector: &Vec<T>) -> u64 { todo!() } }
Let’s dig deeper into this function signature:
- Notice that we renamed the function and parameters name to something more general:
- Instead of calling the function
available_copies, which is highly specific to the library example, we called itcount_occurrences. This expresses the same behavior but in a more general way! - Similarly, instead of calling the parameters
bookandlibrary_books, we renamed them toelementandvector.
- Instead of calling the function
- After the function name, we added
<T>: this represents a type parameter. In some ways, it is similar to a regular parameter (e.g., element): it’s something that the caller of the function must provide. However, unlike a regular parameter that the caller provides a value for (e.g., a specific book or element), the caller must provide a type for the type parameter.- When reading the function signature, think of
Tas a generic unspecified type. It can take on any type the caller wants to. For example, it may becomeStringif the caller uses this function with strings, orBookif the caller uses books, etc.
- When reading the function signature, think of
- The type of the function parameters are different:
- The element (or book) used to be of type
&Book, now it is&T. - The vector used to be of type
&Vec<Book>, now it is&Vec<T>.
- The element (or book) used to be of type
Imagine what would happen if a caller sets T to be Book. If you plug Book in for T, the signature of the function collapses to the old one!
Functions can take more than one generic parameters. For example, we could have written the function as fn count_occurrences<T, F>(element: &T, vector: &Vec<F>) -> u64 { ... }. However, this allows the caller to provide different types for each type parameter. For example, they could set T to String and F to u32. This does not make a lot of sense for our particular use case, since we want the provided element to have the same type as the elements inside the vector.
OK, let’s go ahead and implement the body of the function:
#![allow(unused)] fn main() { fn count_occurrences<T>(element: &T, vector: &Vec<T>) -> u64 { let mut counter = 0; // e's type is &T for e in vector { if e == element { counter += 1; } } return counter; } }
Try to run this code. We will encounter a familiar compile-time error:
error[E0369]: binary operation `==` cannot be applied to type `&T`
--> src/main.rs:7:10
|
7 | if e == element {
| - ^^ ------- &T
| |
| &T
Rust is unsure of how to compare instances of T with each other. This makes sense! Remember how
previously, Rust was unsure how to compare instances of Book! We had to implement (or derive) PartialEq for Book to explain to Rust what equality checking for Book means.
We need to do something similar here. However, T is not a concrete or known type. We cannot
implement or derive any trait for T, because we do not even know what T is!
Trait Bounds
This is where trait bounds come in. We cannot implement anything for T, but we can pose a constraint about what types callers may plug in for that T. Specifically, we want to say that a caller can use any type they want for T, as long as that type implements PartialEq.
#![allow(unused)] fn main() { fn count_occurrences<T: PartialEq>(element: &T, vector: &Vec<T>) -> u64 { let mut counter = 0; // e's type is &T for e in vector { if e == element { counter += 1; } } return counter; } }
Now this code compiles, because we specific that T must implement PartialEq (and therefore has equality checking defined).
Trait bounds can get complex sometimes, e.g., if we need a type to implement several traits. For that, we can use the where syntax to make expressing these constraints easier. For example:
#![allow(unused)] fn main() { fn count_occurrences<T>(element: &T, vector: &Vec<T>) -> u64 // we could also use T: PartialEq + Clone where T: PartialEq, T: Clone { let mut counter = 0; // e's type is &T for e in vector { if e == element { counter += 1; } } return counter; } }
Putting this all together, we get the following code:
#[derive(PartialEq)] pub struct Book { pub title: String, pub author: String, pub isbn: u64, pub edition: u64, } // `book` is the book the user is looking for. // `library_books` is the vector of books we have available. // the function should return how many matching copies we have in our library. fn count_occurrences<T: PartialEq>(element: &T, vector: &Vec<T>) -> u64 { let mut count = 0; for e in vector { // check if `b`, the book we are currently looking at, matches // the requested book. if element == e { count += 1; } } return count; } fn main() { let library_books: Vec<Book> = vec![ Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 1, }, Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 1, }, Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 2, }, Book { title: String::from("The Art of Computer Programming"), author: String::from("Donald Knuth"), isbn: 33, edition: 1, }, ]; let target_book = Book { title: String::from("Harry Potter and The Philosopher's Stone"), author: String::from("JK Rowling"), isbn: 10, edition: 1 }; let count = count_occurrences(&target_book, &library_books); println!("The library has {} copies", count); }
Notice how we call the count_occurrences function in main. We do not need to explicitly provide
the types we want the generic type parameter T to become. Instead, Rust automatically deduces this from the type of target_book and library_books.
Exercise: Modify the code so that Book does not implement/derive PartialEq. Do you think that code should work? Run it and see if your intuition was correct! Try to understand the error message given what we discussed.
Exercises
Exercise 0: Reading and Quiz
Read chapters 10, 10.1, and 10.2 in the Rust book and answer the quiz at the end of 10.1 and 10.2!
After you answer all the questions in a quiz, you will see a report showing you which of your answers were correct and which were not. Use the rust playground to validate your answers and find out why you might have been incorrect!
Exercise 1: Derive
Create a new Rust project, open it with VSCode, and copy over the last complete code sample into your VSCode.
Let’s say our library also has magazines that users can borrow. A magazine has a name, a month, and a year. The name is a String, the month and year are integers.
- Define a type/struct to represent these magazines.
- Create a vector of magazines for representing what magazines your library has.
- Use
count_occurencesto count how many copies of a particular magazine the library has.
Note: the copy of the magazine must match all of the name, month, and year.
Exercise 2: Custom Trait
Now, define a new custom Trait, call it Checkout. This trait should define a function checkout, for checking out an item from the library.
Implement Checkout for both Book and Magazine. The implementation should print out the information of the item along with “checked out!”.
Hint: how should self be passed to checkout? All you need is to print the item (i.e. read permissions!).
Exercise 3: Generic Function and Trait Bounds
Implement a generic function that takes some item that a user wants to checkout, and a vector of similar items representing the library. It finds the first matching copy of that item in the vector. Removes it from the vector, and checks it out!
Use this function to checkout a book and a magazine from our library!
Hint: consider implementing a non generic version of this function just for books as a starting point!
Hint: how should you pass the item and the vector of items to the function? Do you need to modify either of them?
Hint: what trait bounds should the generic type satisfy? You need to be able to checkout the type, and you also need to check equality to find a matching copy.
Solution for books only: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=30eb603812ba9e72b3965252adb50385
Generic solution: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=435c839e1bd09e80ea6ac0e21a08adbc