Exploring Rust Traits and Generics Through Peaky Blinders
Written on
Chapter 1: Introduction to Rust Traits
Welcome to the dynamic realm of Rust! In this guide, we’ll delve into Traits and Generics, drawing entertaining comparisons with beloved characters from Peaky Blinders to enhance both fun and understanding.
1. Understanding Traits
In Rust, a Trait functions similarly to an individual's character. It outlines a collection of behaviors (methods) that can be enacted. Each person possesses specific skills (like speaking), but they express them differently. Likewise, Rust Traits dictate what a type should accomplish without detailing the method of execution.
Consider the traits of the Shelby family members:
// This trait outlines how to speak, but not the content of speech
trait Shelby {
fn speak(&self);
}
struct ThomasShelby;
struct ArthurShelby;
impl Shelby for ThomasShelby {
fn speak(&self) {
// What Tommy says
println!("I'm Thomas Shelby, and this is my business.");
}
}
impl Shelby for ArthurShelby {
fn speak(&self) {
// What Arthur says
println!("I'm Arthur Shelby, and I fight for my family.");
}
}
fn main() {
let tommy = ThomasShelby;
let arthur = ArthurShelby;
tommy.speak();
arthur.speak();
}
In this example, the Shelby trait specifies a speak method. Both ThomasShelby and ArthurShelby implement this trait, each with their unique approach.
2. Traits with Default Implementations
While Thomas and Arthur are unique, some characters may lack distinct expressions. For these cases, we can establish a default behavior for the speak method:
// This trait defines how to speak and sets a default implementation
trait Shelby {
fn speak(&self) {
// Default implementation
println!("How are ya?");
}
}
struct FinnShelby;
impl Shelby for FinnShelby {}
fn main() {
let finn = FinnShelby;
finn.speak();
}
3. Trait Bounds
Function parameters can be restricted to specific traits. For instance, if we want a Shelby family member to respond to a greeting from a stranger in Birmingham, we can ensure that the meet_and_greet function accepts only those implementing the Shelby trait:
trait Shelby {
fn speak(&self) {
println!("How are ya?");}
}
struct FinnShelby;
impl Shelby for FinnShelby {}
fn meet_and_greet(shelby: impl Shelby) {
println!("Stranger: Nice to meet you!");
print!("Shelby: ");
shelby.speak();
}
fn main() {
let finn = FinnShelby;
meet_and_greet(finn);
}
4. Generics
Generics enable us to write versatile code that accommodates various data types. Suppose the Shelbys are planning a new business endeavor and require a business plan adaptable to different goods:
struct BusinessPlan<T> {
goods: T,
}
fn main() {
let whiskey_plan = BusinessPlan { goods: "Whiskey" };
let amount_plan = BusinessPlan { goods: 100 };
println!("Business Plan 1: {}", whiskey_plan.goods);
println!("Business Plan 2: {}", amount_plan.goods);
}
Here, BusinessPlan is a generic struct that can accept various types for its goods field.
5. Integrating Traits with Generics
Combining Traits and Generics harnesses the advantages of both concepts. Let’s extend Shelby's operations to encompass different business types, each necessitating a unique strategy:
trait Operation {
fn run(&self);
}
struct Legal<T> {
business: T,
}
struct Illegal<T> {
business: T,
}
impl<T> Operation for Legal<T> {
fn run(&self) {
println!("Running a legal business.");}
}
impl<T> Operation for Illegal<T> {
fn run(&self) {
println!("Running an illegal business.");}
}
fn operate_business<T: Operation>(business: T) {
business.run();
}
fn main() {
let legal_business = Legal { business: "Real Estate" };
let illegal_business = Illegal { business: "Gambling" };
operate_business(legal_business);
operate_business(illegal_business);
}
6. Generics with Trait Bounds
Trait bounds establish what functionalities a generic type must support. Imagine a family meeting where only Shelby family members are permitted:
trait Shelby {
fn speak(&self);
}
struct ThomasShelby;
impl Shelby for ThomasShelby {
fn speak(&self) {
println!("I'm Thomas Shelby");}
}
fn attend_meeting<T: Shelby>(member: T) {
member.speak();
}
fn main() {
let tommy = ThomasShelby;
attend_meeting(tommy);
}
7. Associated Types in Traits
Within traits, we can introduce a placeholder type for each implementation of that trait to specify. For instance, each family member might have a Role trait with varying types of duties:
trait Role {
type Duty;
fn perform_duty(&self, duty: Self::Duty);
}
struct ArthurShelby;
impl Role for ArthurShelby {
type Duty = String; // Arthur's duty as a task represented as a String
fn perform_duty(&self, duty: Self::Duty) {
println!("Arthur's duty: {}", duty);}
}
struct FinnShelby;
impl Role for FinnShelby {
type Duty = i32; // Finn's duties are typically numerical, like shipment counts
fn perform_duty(&self, duty: Self::Duty) {
println!("Finn's number of shipments to manage: {}", duty);}
}
fn main() {
let arthur = ArthurShelby;
let finn = FinnShelby;
arthur.perform_duty(String::from("Negotiate with the New York gangs"));
finn.perform_duty(15);
}
8. Building Supertraits
We can create new traits based on existing ones using supertraits. Each family member's traits build upon one another, akin to stating, "To be a Shelby Leader, one must first be a Shelby."
trait Shelby {
fn identity(&self) -> String;
}
trait ShelbyLeader: Shelby {
fn make_decision(&self);
}
struct TommyShelby;
impl Shelby for TommyShelby {
fn identity(&self) -> String {
String::from("Tommy Shelby")}
}
impl ShelbyLeader for TommyShelby {
fn make_decision(&self) {
println!("{} decides to expand to America.", self.identity());}
}
fn main() {
let tommy = TommyShelby;
println!("I am {}", tommy.identity());
tommy.make_decision();
}
9. Employing the Newtype Pattern
When we need to implement a trait for an externally created type, the Newtype pattern offers a solution by allowing us to wrap the external type in a new struct:
// Assume ExternalTool is a type from a crate we do not own
struct ExternalTool;
// We create a NewType wrapping ExternalTool
struct ShelbyTool(ExternalTool);
// We can now implement traits on ShelbyTool, even if they are external
trait Adapt {
fn adapt(&self);
}
impl Adapt for ShelbyTool {
fn adapt(&self) {
println!("Adapting the tool for Shelby's use...");}
}
fn main() {
let tool = ShelbyTool(ExternalTool);
tool.adapt();
}
That's all! I hope you found these engaging comparisons helpful in understanding Traits and Generics in Rust. If you enjoyed this guide, please show your appreciation by hitting the clap 👏 button. Rust on!
Chapter 2: Video Tutorials
In this comprehensive guide, we dive deep into Rust Traits, exploring their functionalities and use cases, perfect for beginners.
Learn how to define common behaviors in Rust using Generics and Traits, illustrated with practical examples.