Hello! Welcome to the next lesson in our journey to build a chess game in Rust.
In our last session, we saw how to use the #[derive] attribute to automatically give our structs and enums useful behaviors like printing (Debug), comparing (PartialEq), and copying (Clone, Copy). These behaviors are defined by traits from Rust's standard library.
Today, we're taking the next logical step. Instead of just using traits provided by Rust, we will define our own custom traits to abstract shared behavior and implement them for our structs. This is a cornerstone of building flexible and well-organized programs in Rust. For our chess game, this concept is crucial. All pieces can move, but a Rook's movement logic is very different from a Knight's. Traits will allow us to create a common "language" for piece movement that each piece type can implement in its own unique way.
1. From Interface to Trait: A Familiar Concept
Given your experience with Java, you're likely familiar with interfaces, which define a "contract" of methods that a class must implement. Rust's traits serve a very similar purpose. They define a set of methods that a type must provide, allowing different types to share common functionality.
Let's start with a simple, beginner-friendly article that explains this "contract" analogy very well.
Understanding Rust Traits: For a beginner
This article from Medium introduces traits using the idea of a 'promise' or 'contract,' which should feel familiar.
Read the sections 'What Even Is a Trait?' and 'Let’s Write Some Code'. Focus on the 'driving school' analogy and the basic syntax for defining the Summary trait. Notice the semicolon ; at the end of the function signature, which indicates there is no default implementation.
Now, let's see this in action with a practical coding demonstration. This video gets straight to the point of defining a trait.
Let's Learn Rust: Structs and Traits
This clip from the Engineer Man channel will show you the exact syntax for defining a custom trait, including methods that must be implemented and methods that can have a default implementation.
Watch the segment from 08:09 to 09:27. Pay attention to the difference between the reverse method (which has no body and must be implemented by the user) and the output_rev method (which has a default body).
As you've seen, the core syntax is straightforward:
trait MyBehavior {
// A method signature that any implementing type must provide.
fn required_method(&self) -> String;
// A method with a default implementation.
// Implementing types can use this directly or override it.
fn default_method(&self) {
println!("This is a default behavior.");
}
}
2. Fulfilling the Contract: Implementing a Trait
Once you've defined a trait, you need to "fulfill the contract" for your structs. This is done using an impl block with the syntax impl TraitName for StructName.
Let's see how to implement the trait we just saw defined.
Let's Learn Rust: Structs and Traits
Continuing with the same video, let's see how to fulfill the trait's 'contract' for a specific struct.
Watch from 09:27 to 10:55. Notice the impl Transform for MyType syntax. The compiler forces the developer to provide the code for the reverse method because it was defined without an implementation in the trait. Afterwards, you can call both the required and default methods on an instance of MyType.
To reinforce this, the Medium article provides another excellent, fully commented example of defining and implementing a trait.
Understanding Rust Traits: For a beginner
Let's look at a complete, well-commented code example that ties everything together, from defining the trait to implementing it for multiple structs.
Scroll down to the 'Full code snippet' at the end of the article. Read through the comments in the code block. It clearly labels each step: defining the Summary trait, defining two different structs (NewsArticle, Tweet), and then implementing Summary for each of them with custom logic.
3. The Power of Traits: Polymorphism and Abstraction
So, why go to all this trouble? Because traits allow you to write functions that operate on any type that has the required behavior. This is known as polymorphism, and it's a powerful tool for writing clean, reusable code.
Since you're familiar with OOP, the next video will be very insightful. It directly contrasts Java's class-based approach with Rust's trait-based system.
This video from Let's Get Rusty is perfect for your background. It explains how Rust achieves polymorphism using traits, which is analogous to using interfaces in Java.
Watch from 01:35 to 02:02 and then the more detailed section from 02:25 to 05:05. The key takeaway is the two ways Rust functions can accept any type with a certain trait: Generics (Static Dispatch): fn my_func<T: MyTrait>(item: &T) Trait Objects (Dynamic Dispatch): fn my_func(item: &dyn MyTrait) Pay attention to the brief explanation of when to use each.
This concept is not just theoretical; it's exactly how many game engines are designed. Let's look at a real-world example from a blog post about building a game engine in Rust.
Developing a Game Engine in Rust
To connect this directly to your goal of building a game, let's examine how a Rust game engine uses traits to manage game objects.
First, read the section 'Trait-based Entity Functionality'. Notice how traits like Renderable and Interactive are defined to abstract behavior. Then, look at the section 'Entity Containers'. The code in this section (especially near the end) shows how you can create a Vec that holds different types of game objects, as long as they all implement the same trait, using dyn Trait (what the video called a 'trait object').
4. Hands-On: A Movement Trait for Our Chess Pieces
Let's apply this directly to our chess project. We'll create a new project to prototype this idea.
- Create a new project:
cargo new piece-movement - Open
src/main.rsand replace the contents with the following code. This code uses thestructs andenums from our last lesson and introduces a newPieceMovementtrait.
// Use the data structures we designed in the previous lesson
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Square {
pub file: u8, // 0-7 for 'a'-'h'
pub rank: u8, // 0-7 for '1'-'8'
}
// 1. DEFINE THE TRAIT (the "contract")
// This trait defines a behavior that all moving pieces must have.
pub trait Movable {
// It requires one method, `generate_moves`.
// This method will calculate all valid destination squares from a given position.
fn generate_moves(&self, position: Square) -> Vec<Square>;
}
// 2. DEFINE THE STRUCTS FOR DIFFERENT PIECE TYPES
// We'll create simple, empty structs to represent the pieces for now.
#[derive(Debug)]
pub struct Rook;
#[derive(Debug)]
pub struct Knight;
// 3. IMPLEMENT THE TRAIT FOR EACH STRUCT
// Here, we "fulfill the contract" for Rook.
impl Movable for Rook {
fn generate_moves(&self, position: Square) -> Vec<Square> {
println!("Calculating moves for a Rook at ({}, {})...", position.file, position.rank);
// In future lessons, we will add the real logic here.
// For now, we return an empty list.
vec![]
}
}
// And we do the same for Knight, with its own (future) logic.
impl Movable for Knight {
fn generate_moves(&self, position: Square) -> Vec<Square> {
println!("Calculating moves for a Knight at ({}, {})...", position.file, position.rank);
// L-shaped move logic will go here eventually.
vec![]
}
}
// 4. USE THE TRAIT FOR ABSTRACTION
// This function can accept ANY type that implements the `Movable` trait.
fn print_moves<T: Movable>(piece: T, position: Square) {
println!("--- Finding moves for {:?} ---", piece);
let moves = piece.generate_moves(position);
println!("Possible moves: {:?}", moves);
println!("--------------------------\n");
}
fn main() {
let rook = Rook;
let knight = Knight;
let start_square = Square { file: 3, rank: 3 }; // d4
// We can call our generic function with a Rook...
print_moves(rook, start_square);
// ...and with a Knight! The function doesn't care about the concrete type,
// only that it has the `Movable` behavior.
print_moves(knight, start_square);
}
Run cargo run in your terminal. You'll see that the print_moves function works correctly for both the Rook and the Knight, calling the appropriate implementation of generate_moves for each. This is the power of abstraction using traits!
Conclusion
Excellent work! You've just mastered one of the most fundamental and powerful features of Rust. By defining your own traits, you can create clean, modular, and highly reusable code—an essential skill for building a project as complex as a chess game.
Key Takeaways:
- A trait is a "contract" that defines a collection of methods a type must implement, similar to an
interfacein Java or C#. - You define a trait using the
traitkeyword and implement it for a struct usingimpl TraitName for StructName. - Traits can provide default method implementations, which can be used or overridden by the implementing type.
- Traits enable polymorphism, allowing you to write generic functions (e.g.,
fn func<T: MyTrait>(...)) or use collections of different types (e.g.,Vec<Box<dyn MyTrait>>) that share a common behavior. - For our chess game, we will use a trait to abstract the move generation logic for all the different pieces.
In our next lesson, we'll address another key aspect of large projects: organization. We'll learn how to organize our code into separate files and modules using the mod and pub keywords. This will allow us to keep our growing chess logic tidy and manageable.