Hello! Let's get started.
In our last lesson, we explored Vec<T>, Rust's dynamic array, which is perfect for storing lists of things like a piece's possible moves. You might have noticed that in one of the final code examples, I added #[derive(Debug, Clone, Copy)] above a struct without much explanation. Today, we're going to dive into what that line does.
This lesson is all about making our custom data types more powerful with minimal effort. Our learning outcome is to derive common traits such as Debug, Clone, Copy, and PartialEq to add standard functionality. A "trait" in Rust is a way to define shared behavior, and "deriving" is a shortcut to have the compiler write the code for that behavior for us. These traits will allow us to easily print, copy, and compare the data structures we'll build for our chess game, like pieces and board squares.
1. What Are Traits and Why "Derive" Them?
In Rust, a trait tells the compiler that a certain type has a specific set of behaviors (methods). This is similar to the concept of an interface in Java. While you can implement these traits manually, Rust provides a convenient #[derive] attribute for several common ones. This attribute instructs the compiler to generate the boilerplate implementation for you.
Let's watch a short video that introduces the concept of traits and the idea of deriving them.
This clip from Cฤtฤ's Bits gives a high-level comparison between Rust traits and Java interfaces and introduces the idea of deriving implementations.
Watch from the beginning to 03:12. Focus on how traits define behavior and how the derive attribute provides an automatic implementation for common traits like Debug and Eq.
Now, let's explore the most common derivable traits one by one and see how they'll help in our chess project.
2. The Debug Trait: Making Structs Printable
You've already been using #[derive(Debug)] to print your structs using the {:?} formatter. Let's formalize this. Without the Debug trait, Rust has no idea how to represent your custom struct as a string for printing.
The 5 Standard Library Traits That Separate Rust Beginners from Experts
This article provides an excellent explanation of the Debug trait. Let's read the first section.
Read the section titled 'The Debug Trait: Essential for Development'. It clearly shows the compiler error you get without Debug and how to fix it.
Hands-On: Debug a Board Square
Let's apply this to a Square struct for our chess game.
- Create a new project with
cargo new traits-practiceand opensrc/main.rs. - Define a
Squarestruct, create an instance, and try to print it.
struct Square {
file: char, // 'a' through 'h'
rank: u8, // 1 through 8
}
fn main() {
let e4 = Square { file: 'e', rank: 4 };
// This line won't compile!
println!("The square is: {:?}", e4);
}
If you run cargo run, you'll get a compiler error: Square doesn't implement std::fmt::Debug.
Now, fix it by adding #[derive(Debug)] above the struct definition:
#[derive(Debug)] // Add this line
struct Square {
file: char,
rank: u8,
}
fn main() {
let e4 = Square { file: 'e', rank: 4 };
println!("The square is: {:?}", e4);
}
Run it again. It now works! You can also use {:#?} for "pretty-printing," which is helpful for larger structs.
3. The PartialEq Trait: Making Structs Comparable
Next up is PartialEq, which stands for "partial equality." Deriving this trait allows you to use the == (equals) and != (not equals) operators on your structs. This will be essential for our chess game. For example: Is this piece's position equal to square e4?
Let's go back to the same article to see how it works.
The 5 Standard Library Traits That Separate Rust Beginners from Experts
The section on PartialEq in the same article clearly explains its purpose.
Read the section titled 'PartialEq: Enabling Equality Comparisons'.
Hands-On: Compare Two Squares
Let's update our Square struct.
- Create two
Squareinstances with the same value and try to compare them.
#[derive(Debug)]
struct Square {
file: char,
rank: u8,
}
fn main() {
let a1 = Square { file: 'a', rank: 1 };
let another_a1 = Square { file: 'a', rank: 1 };
// This line won't compile!
if a1 == another_a1 {
println!("The squares are the same.");
}
}
Again, cargo run will give you an error: binary operation ==cannot be applied to typeSquare``.
- Fix this by adding
PartialEqto yourderiveattribute. You can list multiple traits inside the parentheses.
#[derive(Debug, PartialEq)] // Add PartialEq here
struct Square {
file: char,
rank: u8,
}
fn main() {
let a1 = Square { file: 'a', rank: 1 };
let another_a1 = Square { file: 'a', rank: 1 };
let b2 = Square { file: 'b', rank: 2 };
if a1 == another_a1 {
println!("a1 and another_a1 are the same: {}", a1 == another_a1);
}
if a1 != b2 {
println!("a1 and b2 are different: {}", a1 != b2);
}
}
Now it works! The compiler automatically generates code that compares each field of the struct for equality.
4. Clone vs. Copy: Duplicating Your Data
This is a crucial topic that ties directly back to Rust's ownership model. By default, when you assign a variable that owns its data (like a struct) to another variable, ownership is moved.
let s1 = Square { file: 'h', rank: 8 };
let s2 = s1;
// println!("{:?}", s1); // This would be a 'use of moved value' error!
What if we need a duplicate of s1? This is where Clone and Copy come in.
Clone: An explicit, potentially "expensive" way to create a deep copy of a value. You must call the.clone()method.Copy: An implicit, "cheap" way to duplicate a value. If a type isCopy, assignments create a copy instead of moving ownership. This only works for simple types whose data is entirely on the stack.
This video provides an excellent distinction between the two.
๐ฆ Rust Copy trait VS Clone trait
This video from Amirreza Ask clearly explains the differences between the Copy and Clone traits.
Watch from 00:42 to 04:00. Pay close attention to the key differences he highlights: implicit vs. explicit, cheap vs. expensive, and the ability to implement Clone with custom logic.
The article we've been using also has a great section on this, including a critical limitation.
The 5 Standard Library Traits That Separate Rust Beginners from Experts
Let's read the final section from the article to solidify our understanding and see the limitations.
Read the section 'Clone and Copy: Duplicating Values'. Focus on the example where deriving Copy fails because one of the struct's fields (String) is not Copy.
Hands-On: Clone and Copy a Square
Let's experiment with our Square struct.
First, let's enable explicit cloning.
#[derive(Debug, PartialEq, Clone)] // Add Clone
struct Square {
file: char,
rank: u8,
}
fn main() {
let s1 = Square { file: 'h', rank: 8 };
let s2 = s1.clone(); // We explicitly clone it.
// Both are valid because s2 is a new copy!
println!("s1 = {:?}, s2 = {:?}", s1, s2);
assert_eq!(s1, s2);
}
This works as expected. Now, let's see what happens when we make Square a Copy type. A type that is Copy must also be Clone.
#[derive(Debug, PartialEq, Clone, Copy)] // Add Copy
struct Square {
file: char,
rank: u8,
}
fn main() {
let s1 = Square { file: 'h', rank: 8 };
let s2 = s1; // No .clone() needed! An implicit copy is made.
// Both are still valid! Ownership of s1 was not moved.
println!("s1 = {:?}, s2 = {:?}", s1, s2);
assert_eq!(s1, s2);
}
Because char and u8 are both simple types that implement Copy, our Square struct can also implement Copy. This makes them very convenient to pass around.
5. Applying Traits to Our Chess Project
Now, let's lay the groundwork for our chess game's data structures. We'll need types for piece color, piece type, and the piece itself. Since these will be simple data containers, they are perfect candidates for deriving these traits.
Let's define our core chess enums and structs in src/main.rs.
// All these enums and structs contain simple data, so we can
// derive all four traits for maximum convenience.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Color {
White,
Black,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PieceType {
Pawn,
Knight,
Bishop,
Rook,
Queen,
King,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Piece {
pub color: Color,
pub piece_type: PieceType,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Square {
pub file: u8, // 0-7 for files 'a'-'h'
pub rank: u8, // 0-7 for ranks '1'-'8'
}
fn main() {
let white_king = Piece {
color: Color::White,
piece_type: PieceType::King,
};
let e1 = Square { file: 4, rank: 0 };
// We can print them (thanks to Debug)
println!("Piece: {:?}, on Square: {:?}", white_king, e1);
// We can copy them easily (thanks to Copy)
let king_clone = white_king;
let king_on_e1 = (king_clone, e1);
// We can still use the original!
println!("Original piece is still valid: {:?}", white_king);
// We can compare them (thanks to PartialEq)
if king_on_e1.0.piece_type == PieceType::King {
println!("It's a king!");
}
}
By adding one lineโ#[derive(Debug, Clone, Copy, PartialEq)]โto each of our data structures, we've given them a whole suite of useful behaviors that will make writing our game logic much simpler. We don't have to worry about ownership issues when passing a Square to a function, and we can easily check if a Piece is a Pawn.
Conclusion
Great job! You've learned how to leverage one of Rust's most powerful and convenient features to enhance your custom types.
Key Takeaways:
- Traits add behavior to types, similar to interfaces in other languages.
- The
#[derive]attribute asks the compiler to write the implementation for common traits automatically. Debugis essential for printing your custom types for debugging purposes using{:?}.PartialEqallows you to compare instances of your types using==and!=.Cloneprovides an explicit.clone()method for creating a deep copy of a value.Copyenables implicit, cheap copying on assignment, but it can only be used on types whose fields are all alsoCopy.
In our next lesson, we will go one step further. Instead of just using traits provided by Rust, we will learn how to define our own custom traits. This is the key to abstracting behavior, and it will allow us to create a unified way to handle move generation for all the different types of chess pieces.