Notes on Traits and You: A Deep Dive Part 2

Original content by Nell Shamrell-Harrington

When looking for resources to help me better understand Rust Traits, I found this videofrom Nell Shamrell-Harrington. It's a great intro and overview into using Traits, which can be a little confusing when you first start learning Rust.

So these are my notes on this talk, if you haven't watched it and are struggling with learning Traits, I strongly suggest that you watch the video above before you do anything else. But if you're short on time and just want to reference my notes -- here they are. Disclaimer: these notes are my own interpretation of the talk and may not necessarily reflect the views of the original speaker. These note are primarily for me to learn these concepts, and I publish them for others to see and maybe benefit from. Feel free to let me know if I have errors or have misrepresentations.

Introduction

The talk covers 3 different ways that we can use Traits, I have already written my first part of these notes, you can see those linked below.

Nell brilliantly uses D&D as a metaphor to help explain Traits. It should be clear that the use of D&D is just to help grasp some concepts, and the D&D rules do not strictly apply in these concepts, so now that we have that out of the way let's begin.

Traits 201: Trait Bounds

Today we cover Traits Bounds. In my previous notes we used 4 different races to explain how traits can be used with those races. Now, we're going to add a 5th race: the Half-Elf.

Here's our full list:

  1. Dwarves
  2. Elf
  3. HalfOrc
  4. Human
  5. Half-Elf

Traits 101

In our previous note we went through the process of how to define a Trait, we used Constitution as the trait we wanted to apply to the 4 different races in a couple of different ways. If you haven't read that note, it might help to do so first to help better understand the basics of creating our characters, creating a Trait, defining race specific values as well as default values.

Trait Bounds

To help us understand Trait bounds, let's look at how these races communicate. Each race has core languages that they speak, for example.

  • Dwarves: Common, Dwarvish
  • Elf: Common, Elvish
  • Half-Elf: Common, Elvish

Defining the Elvish language Trait

Let's focus on the Elvish language first and define a trait for the language and define a impl for the Elf and HalfElf races.

// Define the Elvish trait
pub trait Elvish {

}

// Implement the Elvish trait for the Elf struct
impl Elvish for Elf {

}

// Implement the Elvish trait for the HalfElf struct
impl Elvish for HalfElf {

This should be pretty straight-forward so far as it's just like we started Traits 101.

Now let's make a function to allow our characters to actually speak Elvish

pub fn speak_elvish(character: T) -> String{
	String::from("yes")
}

Right now this function can be called on anything with no arguments, so we add an argument called character, but this character doesn't need to be a specific type it can be any of the structs we've already created.

But if we add the <T: Elvish> this means we will only accept the Elvish trait.

pub fn speak_elvish<T: Elvish>(character: T) -> String {
	String::from("yes")
}

let my_elf = Elf { name: String::from("ElleElf")};

// we use our previously defined variable with the `speak_elvish` function
speak_elvish(my_elf)

So now if we call our Elf (my_elf) with speak_elvish(my_elf) it would return a yes because we have implemented Elvish for Elf.

Let's look at our HalfElf

// This is the same function that we defined before
pub fn speak_elvish<T: Elvish>(character: T) -> String {
	String::from("yes")
}
// here we bind our HalfElf to `my_half_elf`
let my_half_elf = HalfElf { name: String::from("HarryHalfElf")};

speak_elvish(my_half_elf) // again this returns 'yes' since we've implemented `Elvish` for `HalfElf`

So far so good right, I hope it's pretty easy to see why both the my_elf and my_half_elf both return yes when we call them using the speak_elvish function.

HalfOrcs Incoming

We've successfully implemented our speak_elvish trait on both our Elf type races, but let's see what happens when we try to call our HalfOrc.

pub fn speak_elvish<T: Elvish>(character: T) -> String {
	String::from("yes")
}

let my_half_orc = HalfOrc { name: String::from("OscarOrc")};

speak_elvish(my_half_orc)

So what happens if wet try to call my_half_orc with speaks_elvish? Well in this case our code would not compile because we have not implemented Evlish for HalfOrc. Of course, Orcs tend to break everything 😉.

Recap

Trait bounds allow a function to only accept types that implement a certain trait or combination of traits. This provides a powerful way to enforce specific behaviors and characteristics for the types used in your functions. Furthermore, you can combine multiple traits as criteria for a function, allowing for even greater flexibility and control.

Let's walk through each step like we did in Traits 101 on how we define Trait bounds:

  1. Define our Trait in our case it was the language trait Elvish
  2. Implement Elvish for the races that speak it, in our case Elf and HelfElf.
  3. Define a function speak_elvish that only accepts the race we define within the function. In our case we did it for both Elf and HalfElf. This function returns a yes if we call a race that has Elvish implemented, for example: speak_elvish(my_half_elf)
  4. If we try to do the same with our HalfOrc: speak_elvish(my_half_orc), our code won't compile.

This was a short and straightforward introduction to Trait Bounds, and we'll dive deeper into Rust Traits in the upcoming post on Traits 301: Trait Objects, where the real magic happens