Hello! Welcome back.
In our last lesson, we built a skeleton for our piece movement logic using the match expression. You might have noticed that in some of the examples we looked at, match was often used with a type called Option<T>. Today, we're going to focus entirely on this type. It's one of Rust's most important and elegant features for writing safe and robust code.
Our goal is to understand how to handle the possibility of an absent value using the Option<T> enum. This concept is fundamental to Rust and directly addresses a critical question for our chess game: how do we represent an empty square on the board?
1. The Problem of Absence and Rust's Solution
In languages like C and Java, the concept of an absent value is often represented by null. While useful, null is also a notorious source of bugs. If you try to use a null value as if it were a real object, your program will crash—think of the infamous NullPointerException in Java. You are forced to remember to check for null everywhere, and if you forget, your program is at risk.
Rust takes a different approach by getting rid of null entirely. Instead, it uses a special enum called Option<T> to encode the possibility of a value being absent directly into the type system.
Let's watch a short video that introduces this core concept.
Enums and Pattern Matching in Rust
This clip from the Let's Get Rusty channel explains what the Option enum is and why it's a safer alternative to null values found in other languages.
Watch from 04:16 to 05:05. Focus on the two variants of the Option enum: Some(T) and None.
As the video explains, Option<T> has two possible variants:
Some(T): The value exists and is of typeT.None: The value is absent.
Because Option<T> is an enum, the Rust compiler can force us to handle both the Some and None cases, preventing us from accidentally using a value that isn't there.

When you declare a variable, if the compiler can infer the type inside Some, you don't need to specify it. However, if you're starting with None, you must tell the compiler what type of Option it is.
Enums and Pattern Matching in Rust
This next segment demonstrates the syntax for creating Option variables, including the important case where you must provide a type annotation for None.
Watch from 05:05 to 05:42. Pay attention to why let z = None; would cause an error, but let z: Option<i32> = None; is correct.
2. Working with Option Values
So, if a value is wrapped in an Option, how do we use it? You can't just perform operations on it directly. An Option<i32> is a different type from an i32. This is a key part of Rust's safety guarantee.
Enums and Pattern Matching in Rust
This short clip perfectly illustrates why you can't add an integer to an Option<i32>. It causes a compile-time error, protecting you from potential runtime bugs.
Watch from 05:32 to 06:12. The video demonstrates that you must first extract the value from the Some variant before you can use it.
To safely get the value out of an Option, we need to handle both possibilities. This is a perfect job for the match expression we learned about in the last lesson!
Let's look at a complete example of a function that takes an Option, operates on the value if it exists, and returns a new Option.
Enums and Pattern Matching in Rust
This final clip shows a complete function, plus_one, that demonstrates the idiomatic way to handle Option<T> using a match expression.
Watch from 08:41 to 10:21. Follow the logic for both when the function receives Some(5) and when it receives None.
Let's review the structure from the video. It's a very common pattern in Rust:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None, // If we get nothing, we return nothing.
Some(i) => Some(i + 1), // If we get a value `i`, we return a new Some with `i + 1`.
}
}
This is the essence of working with Option: you use match to check which variant you have, and then you define what to do in each case. This compile-time check is what makes Option so much safer than null.
3. Hands-On: Modeling the Chess Board
Now, let's apply this directly to our chess game. An 8x8 chessboard has 64 squares. Some of those squares will have a piece on them, and some will be empty. This "has a piece or is empty" scenario is exactly what Option<T> was designed for.
We can represent the entire board as a collection of Option<Piece>.
Some(Piece)will mean a square is occupied by a specific piece.Nonewill mean the square is empty.
This is not just a theoretical idea; it's how experienced Rust developers would model this problem.
Board Design from rust-chess project
Take a look at the design description from a complete Rust chess project on GitHub. The author explains exactly why they chose this approach.
Read the section titled 'Structure and Design', focusing on the 'Board' subsection. The key sentence is: 'The Vec<Option<Piece>> structure was necessary... and I chose to populate it with Piece options because each cell may or may not have a piece.'
Let's implement this. We will create a Board struct to hold the state of our 64 squares.
Task: Create the Board struct.
-
Prepare your
Piecestruct: ForOption<Piece>to work in an array easily, ourPiecestruct needs to beCopy. A struct can beCopyif all of its fields areCopy. OurPieceKindandPieceColorenums are simple enumerations, so they can beCopy. Add#[derive(Copy, Clone, Debug)]above the definitions forPieceKind,PieceColor, andPiece. TheDebugtrait will let us print the board for testing. -
Define the
Boardstruct: Add a new struct namedBoard. It will contain one field,squares, which will be a fixed-size array of 64Option<Piece>elements. -
Implement a
new()function: In animplblock forBoard, create anew()function that returns aBoardwith all squares initialized toNone. -
Test it in
main: In yourmainfunction, create a new board and print it to see the result.
Here's the code you'll need. Add it to your src/main.rs file.
// Add these `derive` attributes to your enums and struct
#[derive(Copy, Clone, Debug)]
pub enum PieceKind {
Pawn,
Rook,
Knight,
Bishop,
Queen,
King,
}
#[derive(Copy, Clone, Debug)]
pub enum PieceColor {
White,
Black,
}
#[derive(Copy, Clone, Debug)]
pub struct Piece {
kind: PieceKind,
color: PieceColor,
}
// ... your `impl Piece` block ...
// --- ADD THE NEW CODE BELOW ---
// A struct to represent the game board
#[derive(Debug)]
struct Board {
// An array of 64 squares. Each square may or may not contain a piece.
squares: [Option<Piece>; 64],
}
impl Board {
// Creates a new, empty board.
fn new() -> Self {
Board {
// Initializes all 64 squares to `None`.
squares: [None; 64],
}
}
}
fn main() {
// Create a new empty board.
let board = Board::new();
// Print the board state. The `{:?}` formatter uses the `Debug` trait.
println!("Initial empty board: {:?}", board);
}
When you run this with cargo run, you should see a long output showing an array of 64 Nones. Congratulations! You've just created the fundamental data structure for our chess game, using one of Rust's most powerful safety features.
Conclusion
Today you've learned how to handle the possibility of absence in a safe, explicit, and robust way. This is a huge step up from the error-prone null-based approaches in other languages.
Key Takeaways:
- Rust uses the
Option<T>enum to represent a value that might be absent, avoiding the need fornull. Option<T>has two variants:Some(T)for an existing value andNonefor an absent one.- The Rust compiler enforces that you handle both
SomeandNonecases, typically using amatchexpression. - We applied this concept to our chess game by modeling the board as an array of
Option<Piece>, whereNonerepresents an empty square.
In our next lesson, we'll explore another crucial enum called Result<T, E>. While Option deals with values that might be there or not, Result is used to handle operations that can either succeed (returning a value) or fail (returning an error). This is key for building resilient applications that can gracefully handle problems.