Rust Traits: from Zero to Hero

Who’s never stumbled upon the tricky “the (…) trait bound is not satisfied” when coding in Rust? We have all been there!

Rust Traits: from Zero to Hero

Who’s never stumbled upon the tricky “the (…) trait bound is not satisfied” when coding in Rust? We have all been there!

This article will cover everything you need to know about Rust’s Traits, from the basics like Trait Bounds to more complex topics like Runtime Polymorphism and Trait Aliases.

Whether you’re just starting out or looking to brush up on your Rust skills, we’ve got you covered with clear explanations and practical examples to keep your Traits knowledge sharp!

What are Traits, at all?

Traits in Rust serve as a means to define shared behavior across different types. They specify a set of method signatures (and optionally provide default implementations) that implementing types must adhere to. This mechanism facilitates polymorphism and ensures a level of abstraction that enhances code reusability and maintainability.

Basic Trait Definition and Implementation

To begin, let’s define a simple Trait and implement it for a specific type:

trait Describable { 
    fn describe(&self) -> String; 
} 
 
struct Person { 
    name: String, 
    age: u8, 
} 
impl Describable for Person { 
    fn describe(&self) -> String { 
        format!("{} is {} years old.", self.name, self.age) 
    } 
}

In this example, the Describable Trait requires a describe method. The Person struct implements this Trait, providing a concrete description of a person.

Default Methods and Overriding

Traits can provide default method implementations. Implementing types can use these defaults or override them:

trait Describable { 
    fn describe(&self) -> String { 
        String::from("This is an object.") 
    } 
} 
 
struct Product { 
    name: String, 
    price: f64, 
} 
 
// Using the default implementation 
impl Describable for Product {} 
struct Person { 
    name: String, 
    age: u8, 
} 
 
// Overriding the default implementation 
impl Describable for Person { 
    fn describe(&self) -> String { 
        format!("{} is {} years old.", self.name, self.age) 
    } 
}

Here, Product uses the default describe method from Describable, while Person provides its own implementation.

Advanced Trait Usage

As we delve deeper, Rust’s Trait system unveils more sophisticated capabilities such as Trait Bounds, Generic Constraints, and more.

Trait Bounds

Trait Bounds are a pivotal feature for working with generics, allowing you to specify that a generic type must implement a particular Trait:

fn print_description<T: Describable>(item: T) { 
    println!("{}", item.describe()); 
} 
 
// Usage with a `Person` instance 
let person = Person { 
    name: String::from("Alice"), 
    age: 30, 
}; 
 
print_description(person);

This function can accept any type T as long as it implements the Describable Trait.

Multiple Trait Bounds

You can also specify multiple Traits using the + syntax or where clauses for more clarity and flexibility:

trait Identifiable { 
    fn identifier(&self) -> String; 
} 
 
// Using `+` for multiple Trait Bounds 
fn print_detailed_description<T: Describable + Identifiable>(item: T) { 
    println!("{} - {}", item.identifier(), item.describe()); 
} 
// Using `where` clauses 
fn print_detailed_description<T>(item: T) 
where 
    T: Describable + Identifiable, 
{ 
    println!("{} - {}", item.identifier(), item.describe()); 
}

Trait Objects

For runtime polymorphism, Rust provides Trait Objects. This allows for dynamic dispatch of methods at runtime, albeit with some performance cost:

fn print_descriptions(items: &[&dyn Describable]) { 
    for item in items { 
        println!("{}", item.describe()); 
    } 
} 
 
let items: [&dyn Describable; 2] = [&person, &product]; 
 
print_descriptions(&items);

Here, dyn Describable is used to create a heterogeneous collection of items that implement the Describable Trait.

Associated Types and Traits

Traits can also define associated types, providing a way to associate one or more types with the Trait implementations:

trait Container { 
    type Item; 
 
fn contains(&self, item: &Self::Item) -> bool; 
} 
 
struct Bag { 
    items: Vec<String>, 
} 
 
impl Container for Bag { 
    type Item = String; 
    fn contains(&self, item: &Self::Item) -> bool { 
        self.items.contains(item) 
    } 
}

This pattern is particularly useful for Traits that need to work closely with other types.

Trait Inheritance

Traits in Rust can inherit from other Traits, allowing you to create a hierarchy of Traits. This is useful when a more specialized Trait should encompass all functionalities of a more generic one:

trait Named { 
    fn name(&self) -> String; 
} 
 
trait Employee: Named { 
    fn id(&self) -> u32; 
} 
 
struct Programmer { 
    name: String, 
    id: u32, 
} 
 
impl Named for Programmer { 
    fn name(&self) -> String { 
        self.name.clone() 
    } 
} 
 
impl Employee for Programmer { 
    fn id(&self) -> u32 { 
        self.id 
    } 
}

In this example, the Employee Trait inherits from the Named Trait, meaning that any Employee must also implement the Named Trait, ensuring that an Employee can always provide a name as well as an ID.

Higher-Ranked Trait Bounds (HRTBs)

Higher-Ranked Trait Bounds (HRTBs) are used in Rust to work with lifetimes on Traits in a more flexible way. This is particularly useful when dealing with closures or iterators that involve lifetimes:

fn call_with_ref<T, F>(value: T, f: F) 
where 
    F: for<'a> FnOnce(&'a T), 
{ 
    f(&value); 
} 
 
let x = 42; 
 
call_with_ref(x, |y| println!("x is {}", y));

Here, F is a function that can take a reference to T with any lifetime. This allows the function call_with_ref to accept a wide variety of closures without running into lifetime issues.

Combining Traits for Extensibility

Traits can be combined to extend the functionality of types in a modular way. This is particularly useful for adding functionalities to types without modifying their definitions:

trait Bark { 
    fn bark(&self); 
} 
 
trait Wag { 
    fn wag(&self); 
} 
 
struct Dog; 
impl Bark for Dog { 
    fn bark(&self) { 
        println!("Woof!"); 
    } 
} 
 
impl Wag for Dog { 
    fn wag(&self) { 
        println!("Wagging tail!"); 
    } 
} 
 
fn make_dog_bark_and_wag(dog: &impl Bark + Wag) { 
    dog.bark(); 
    dog.wag(); 
} 
 
let my_dog = Dog; 
make_dog_bark_and_wag(&my_dog);

In this example, the Dog struct implements both the Bark and Wag Traits, and the make_dog_bark_and_wag function takes any type that implements both of these Traits.

Trait Aliases

Trait aliases in Rust can simplify complex Trait bounds by aliasing a combination of Traits into a single identifier. This can make your code more readable and easier to manage, especially when dealing with multiple Traits:

trait Stringify = std::fmt::Display + std::fmt::Debug; 
 
fn print_stringify<T: Stringify>(item: T) { 
    println!("{}", item); 
    println!("{:?}", item); 
} 
 
struct MyStruct { 
    value: i32, 
} 
 
impl std::fmt::Display for MyStruct { 
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 
        write!(f, "Display: {}", self.value) 
    } 
} 
 
impl std::fmt::Debug for MyStruct { 
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 
        f.debug_struct("MyStruct") 
         .field("value", &self.value) 
         .finish() 
    } 
} 
 
let my_struct = MyStruct { value: 10 }; 
 
print_stringify(my_struct);

Here, the Stringify Trait alias combines std::fmt::Display and std::fmt::Debug, allowing print_stringify to accept any type that implements both.

Operator Overloading with Traits

Rust allows for operator overloading through specific Traits in the std::ops module. This feature can be used to define custom behavior for operators like +, -, *, /, and more:

use std::ops::Add; 
 
struct Point { 
    x: i32, 
    y: i32, 
} 
 
impl Add for Point { 
    type Output = Point; 
    fn add(self, other: Point) -> Point { 
        Point { 
            x: self.x + other.x, 
            y: self.y + other.y, 
        } 
    } 
} 
 
let p1 = Point { x: 1, y: 2 }; 
 
let p2 = Point { x: 3, y: 4 }; 
 
let p3 = p1 + p2; // Uses the `add` method from the `Add` Trait implementation 
 
println!("p3.x: {}, p3.y: {}", p3.x, p3.y);

In this example, the Add Trait is implemented for the Point struct, allowing two Point instances to be added together using the + operator.

Implementing Custom Iterators

Implementing the Iterator Trait allows you to create custom iterators for your types. This is particularly useful when you want to iterate over a custom data structure or when you want to provide a specific iteration behavior:

struct Fibonacci { 
    curr: u64, 
    next: u64, 
} 
 
impl Iterator for Fibonacci { 
    type Item = u64; 
    fn next(&mut self) -> Option<Self::Item> { 
        let new_next = self.curr + self.next; 
        self.curr = self.next; 
        self.next = new_next; 
        Some(self.curr) 
    } 
} 
 
fn fibonacci() -> Fibonacci { 
    Fibonacci { curr: 0, next: 1 } 
} 
 
let fib_sequence = fibonacci().take(5); 
 
for num in fib_sequence { 
    println!("{}", num); 
}

This Fibonacci iterator yields an infinite sequence of Fibonacci numbers, and we use .take(5) to get the first five values from the sequence.

Traits for Type Conversion

The From and Into Traits in Rust are used for type conversions. Implementing these Traits can make type conversion between related types more ergonomic:

struct Celsius(f64); 
struct Fahrenheit(f64); 
 
impl From<Celsius> for Fahrenheit { 
    fn from(celsius: Celsius) -> Self { 
        Fahrenheit(celsius.0 * 1.8 + 32.0) 
    } 
} 
 
let temp_c = Celsius(0.0); 
 
let temp_f: Fahrenheit = temp_c.into(); // Converts Celsius to Fahrenheit using `From`/`Into` 
 
println!("{}C is {}F", temp_c.0, temp_f.0);

In this example, we define two types for temperature in Celsius and Fahrenheit and implement From<Celsius> for Fahrenheit. This automatically provides an implementation for Into<Fahrenheit> for Celsius, allowing for easy conversion between these types.

🚀 Explore More by Luis Soares

📚 Learning Hub: Expand your knowledge in various tech domains, including Rust, Software Development, Cloud Computing, Cyber Security, Blockchain, and Linux, through my extensive resource collection:

  • Hands-On Tutorials with GitHub Repos: Gain practical skills across different technologies with step-by-step tutorials, complemented by dedicated GitHub repositories. Access Tutorials
  • In-Depth Guides & Articles: Deep dive into core concepts of Rust, Software Development, Cloud Computing, and more, with detailed guides and articles filled with practical examples. Read More
  • E-Books Collection: Enhance your understanding of various tech fields with a series of free e-Books, including titles like “Mastering Rust Ownership” and “Application Security Guide” Download eBook
  • Project Showcases: Discover a range of fully functional projects across different domains, such as an API Gateway, Blockchain Network, Cyber Security Tools, Cloud Services, and more. View Projects
  • LinkedIn Newsletter: Stay ahead in the fast-evolving tech landscape with regular updates and insights on Rust, Software Development, and emerging technologies by subscribing to my newsletter on LinkedIn. Subscribe Here

🔗 Connect with Me:

  • Medium: Read my articles on Medium and give claps if you find them helpful. It motivates me to keep writing and sharing Rust content. Follow on Medium
  • Personal Blog: Discover more on my personal blog, a hub for all my Rust-related content. Visit Blog
  • LinkedIn: Join my professional network for more insightful discussions and updates. Connect on LinkedIn
  • Twitter: Follow me on Twitter for quick updates and thoughts on Rust programming. Follow on Twitter

Wanna talk? Leave a comment or drop me a message!

All the best,

Luis Soares
luis.soares@linux.com

Senior Software Engineer | Cloud Engineer | SRE | Tech Lead | Rust | Golang | Java | ML AI & Statistics | Web3 & Blockchain

Read more