Hello! Let's get started.
In our last lesson, we defined the basic data structures for our chess game: a Piece struct that holds a PieceKind enum and a PieceColor enum. These are excellent for representing the state of a piece, but right now, they're just passive containers for data. They don't do anything.
Today, we'll change that. We're going to give our structs and enums behavior by implementing methods. This is how you associate functions with your custom types in Rust, allowing them to perform actions or answer questions about their own data. For you, coming from C and Java, this is Rust's way of attaching functionality to a data structure, much like methods within a Java class or functions operating on a C struct.
By the end of this lesson, you will be able to add functions directly to your Piece, PieceKind, and PieceColor types, making your code more organized and expressive.
1. The impl Block: Giving Your Types Behavior
In Rust, you add behavior to a struct or enum using an impl block (short for "implementation"). Any functions defined inside an impl block are associated with that type.
There are two kinds of functions you can write inside impl:
- Methods: These operate on a specific instance of a type. Their first parameter is always
self, which represents the instance the method is being called on. You've seen this concept in other languages (likethisin Java). They are called using dot notation:my_instance.do_something(). - Associated Functions: These are associated with the type itself, not a specific instance. They do not take
selfas a parameter. They are often used as "constructors" to create new instances of a struct. They are called using double-colon::syntax:MyType::create_something().
To get a solid grasp of this, let's look at a clear explanation and example.
Implementing structs and enums - Easy Rust
The "Easy Rust" guide provides a fantastic, no-nonsense introduction to impl blocks. This reading will walk you through the core concepts and a complete example.
Read the first two sections of the page. Start from the top ('Implementing structs and enums') and continue down through the first full code example about the Animal struct. Pay close attention to: The difference between an associated function (new()) and methods (&self, &mut self). The meaning of &self (immutable borrow) and &mut self (mutable borrow), which builds on what we learned about ownership.
The video version of this guide is also excellent if you prefer a visual walkthrough. It covers the same concepts presented in the text.
Easy Rust 036: Implementing structs and enums 2
This video from the "Easy Rust" series demonstrates the same Animal example, showing how to define and call methods on a struct.
Watch from 03:02 to 05:26. This covers: 03:02 - 03:47: Implementing the new() associated function. 03:47 - 04:38: Implementing methods with &mut self to change the struct's data. 04:38 - 05:26: Implementing a method with &self to read the struct's data.
2. Hands-On: Adding Methods to Our Piece Struct
Now, let's apply this directly to our chess project. In the last lesson, we created Piece instances but noted we couldn't even print them to the console. Let's fix that and add our first methods.
A common need in our game will be to ask a piece what its color or kind is. We can create methods for this.
Task 1: Making our types printable and adding methods.
- Open your
rust_chessproject insrc/main.rs. - Modify the code to match the version below. I'll explain the changes right after.
- Run it with
cargo run.
// In src/main.rs
// Add the `derive` attribute to make this enum printable and comparable.
// We'll learn more about `derive` in a future lesson.
#[derive(Debug, PartialEq)]
enum PieceKind {
Pawn,
Rook,
Knight,
Bishop,
Queen,
King,
}
#[derive(Debug, PartialEq)]
enum PieceColor {
White,
Black,
}
#[derive(Debug)]
struct Piece {
kind: PieceKind,
color: PieceColor,
}
// Implementation block for our Piece struct
impl Piece {
// An associated function that acts as a constructor.
// It takes the details and returns a new Piece instance.
fn new(kind: PieceKind, color: PieceColor) -> Self {
// `Self` is an alias for the type the `impl` block is for, in this case `Piece`.
Self { kind, color }
}
// A method to get an immutable reference to the piece's color.
// It takes `&self` because it only needs to read the data.
fn color(&self) -> &PieceColor {
&self.color
}
// A method to get an immutable reference to the piece's kind.
fn kind(&self) -> &PieceKind {
&self.kind
}
}
fn main() {
// We now use our `new` associated function to create a piece.
let white_queen = Piece::new(PieceKind::Queen, PieceColor::White);
// Thanks to `#[derive(Debug)]`, we can now print the whole struct!
println!("Created piece: {:?}", white_queen);
// We can call our new methods to get specific info.
println!("The piece's color is: {:?}", white_queen.color());
// Let's test if the kind is what we expect.
// `PartialEq` allows us to use `==`
if white_queen.kind() == &PieceKind::Queen {
println!("It's a Queen!");
}
}
What did we just do?
#[derive(Debug, PartialEq)]: We added this "attribute" above our enums.Debugallows Rust to generate default code to print the type using{:?}.PartialEqgenerates code to allow comparisons with==. This solved the problem from last lesson!impl Piece: We created an implementation block forPiece.fn new(...): This is our associated function. Instead of creating the struct manually withPiece { ... }, we can now callPiece::new(...). This is a very common pattern in Rust for clean object creation.fn color(&self): This is a method. When we callwhite_queen.color(),&selfrefers to an immutable borrow ofwhite_queen. The method returns a reference to thecolorfield inside it.
This structure is much cleaner and is the standard way to build types in Rust.
3. Implementing Methods on Enums
Methods aren't just for structs! You can add them to enums too. This is incredibly useful for adding behavior specific to each variant of an enum.
For example, in chess, each piece type has a rough point value (e.g., a Queen is worth 9 points, a Rook 5, etc.). We can create a method on our PieceKind enum to return this value.
Implementing structs and enums - Easy Rust
The "Easy Rust" guide also has a short, clear example of implementing a method on an enum.
Read the final section of the page, starting with 'Here is one more small example.' Notice how the check method is defined in an impl Mood block and uses match self to handle each variant.
Task 2: Giving our pieces value.
Let's add a value() method to our PieceKind enum. This will give us a chance to use the match expression you just saw, which is a preview of our very next lesson!
Add the following impl block to your src/main.rs file, and then add the new println! statements to your main function.
// Add this `impl` block to your code, e.g., after the PieceKind enum definition.
impl PieceKind {
fn value(&self) -> u8 {
// `match` is perfect for checking which enum variant `self` is.
// We will explore `match` in depth in the next lesson.
match self {
PieceKind::Pawn => 1,
PieceKind::Knight => 3,
PieceKind::Bishop => 3,
PieceKind::Rook => 5,
PieceKind::Queen => 9,
PieceKind::King => 0, // In practice, a King's value is infinite. 0 is a placeholder.
}
}
}
// In main(), add these lines at the end:
fn main() {
// ... (previous code from main)
// We can get the piece's kind, and then call the `value` method on it.
let piece_value = white_queen.kind().value();
println!("The value of a {:?} is {}", white_queen.kind(), piece_value);
}
When you run the full file, you'll see the output The value of a Queen is 9. We've successfully attached behavior directly to our enum!
A Glimpse into a Real Chess Engine
What we've done today is exactly how real-world Rust projects are built. To see how these concepts scale up, take a quick look at this blog post by a developer building a chess AI.
Building a Chess AI, Part 1 โ Game State
This article, "Building a Chess AI, Part 1," shows the data structures for a real chess engine in Rust. You don't need to understand everything, but notice the patterns we learned today.
Skim through the 'Foundations' section. Notice the impl Piece block with a colour method and the impl Square block with its own methods. This shows how methods are used to create a clean interface for interacting with game objects.
Seeing this code should feel encouraging. The structs and impl blocks in that project, while more complex, follow the exact same principles you've just put into practice.
Conclusion
Today we've transformed our passive data structures into active components of our application. This is a huge step in making our code robust and easy to work with.
Key Takeaways:
- You add behavior to structs and enums using an
implblock. - Associated functions (like
new) are linked to the type itself and are called with::. They are commonly used as constructors. - Methods are linked to an instance of a type. They take
&self(to read data) or&mut self(to modify data) and are called with.. - You can, and should, implement methods on both structs and enums to encapsulate logic and keep your code organized.
In our next lesson, we'll take a deep dive into the match keyword you briefly saw today. It is one of Rust's most powerful features and is essential for working with enums and handling different states in our chess game.