Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 13

Implementing Common Traits

Recommended resources

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.

Rust Does Interfaces BETTER!

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.

  1. Create a new project with cargo new traits-practice and open src/main.rs.
  2. Define a Square struct, 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.

  1. Create two Square instances 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``.

  1. Fix this by adding PartialEq to your derive attribute. 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 is Copy, 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.

Rust Trait Derivation Example
This image shows the syntax for deriving multiple traits at once. We'll use this pattern for our chess data types to give them printing, comparison, and copying abilities right out of the box.

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.
  • Debug is essential for printing your custom types for debugging purposes using {:?}.
  • PartialEq allows you to compare instances of your types using == and !=.
  • Clone provides an explicit .clone() method for creating a deep copy of a value.
  • Copy enables implicit, cheap copying on assignment, but it can only be used on types whose fields are all also Copy.

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.