Rust - Advanced Traits

Overview

Estimated time: 45–55 minutes

Dive deep into advanced trait features including default type parameters, supertraits, the orphan rule, operator overloading, and disambiguation. Learn how to design flexible and powerful trait hierarchies.

Learning Objectives

Prerequisites

Default Type Parameters

Traits can have default type parameters, making them easier to use while maintaining flexibility:

use std::ops::Add;

// The Add trait has a default type parameter
trait Add<Rhs = Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}

// Custom type implementing Add with default (Self)
#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {  // Using default Rhs = Self
    type Output = Point;
    
    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

// Implementing Add with different types
impl Add<i32> for Point {  // Specific Rhs = i32
    type Output = Point;
    
    fn add(self, scalar: i32) -> Point {
        Point {
            x: self.x + scalar,
            y: self.y + scalar,
        }
    }
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 3, y: 4 };
    
    // Using default implementation (Point + Point)
    let p3 = p1 + p2;
    println!("Point + Point: {:?}", p3);
    
    // Using specific implementation (Point + i32)
    let p4 = Point { x: 1, y: 2 } + 5;
    println!("Point + i32: {:?}", p4);
}

Expected Output:

Point + Point: Point { x: 4, y: 6 }
Point + i32: Point { x: 6, y: 7 }

Supertraits

Supertraits allow you to require that implementors of a trait must also implement another trait:

use std::fmt::Display;

// Supertrait: any type implementing Summary must also implement Display
trait Summary: Display {
    fn summarize(&self) -> String;
    
    // Default implementation can use supertrait methods
    fn announce(&self) {
        println!("Breaking news! {}", self.summarize());
    }
}

struct Article {
    headline: String,
    content: String,
}

// Must implement Display first (supertrait requirement)
impl Display for Article {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.headline)
    }
}

// Now can implement Summary
impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}: {}", self.headline, 
                &self.content[..50.min(self.content.len())])
    }
}

// Function requiring both traits (automatically satisfied)
fn notify(item: &impl Summary) {
    println!("Notification: {}", item);  // Uses Display
    item.announce();                     // Uses Summary
}

fn main() {
    let article = Article {
        headline: "Rust 1.75 Released".to_string(),
        content: "Rust 1.75 brings new features including...".to_string(),
    };
    
    notify(&article);
}

Expected Output:

Notification: Rust 1.75 Released
Breaking news! Rust 1.75 Released: Rust 1.75 brings new features including...

Multiple Supertraits

use std::fmt::{Debug, Display};

// Multiple supertraits
trait Printable: Debug + Display + Clone {
    fn print_info(&self) {
        println!("Debug: {:?}", self);
        println!("Display: {}", self);
        println!("Clone: {:?}", self.clone());
    }
}

#[derive(Debug, Clone)]
struct Document {
    title: String,
    content: String,
}

impl Display for Document {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "Document: {}", self.title)
    }
}

impl Printable for Document {}  // No additional methods to implement

fn main() {
    let doc = Document {
        title: "Rust Guide".to_string(),
        content: "Learning Rust...".to_string(),
    };
    
    doc.print_info();
}

Expected Output:

Debug: Document { title: "Rust Guide", content: "Learning Rust..." }
Display: Document: Rust Guide
Clone: Document { title: "Rust Guide", content: "Learning Rust..." }

The Orphan Rule

The orphan rule states that you can implement a trait for a type only if you own either the trait or the type. This prevents conflicts:

// ✅ Valid: We own the trait
trait MyTrait {
    fn my_method(&self);
}

impl MyTrait for String {  // We own MyTrait, implementing for foreign type
    fn my_method(&self) {
        println!("MyTrait for String: {}", self);
    }
}

// ✅ Valid: We own the type
struct MyStruct {
    value: i32,
}

impl std::fmt::Display for MyStruct {  // We own MyStruct, implementing foreign trait
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "MyStruct({})", self.value)
    }
}

// ❌ Invalid: We own neither Display nor String
// impl std::fmt::Display for String { ... }  // Compiler error!

// ✅ Workaround: Newtype pattern
struct MyString(String);

impl std::fmt::Display for MyString {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "MyString: {}", self.0)
    }
}

fn main() {
    let s = String::from("Hello");
    s.my_method();
    
    let my_struct = MyStruct { value: 42 };
    println!("{}", my_struct);
    
    let my_string = MyString("World".to_string());
    println!("{}", my_string);
}

Expected Output:

MyTrait for String: Hello
MyStruct(42)
MyString: World

Operator Overloading

Rust supports operator overloading through standard traits:

use std::ops::{Add, Sub, Mul, Index, IndexMut};

#[derive(Debug, Clone, PartialEq)]
struct Matrix {
    data: Vec<Vec<i32>>,
    rows: usize,
    cols: usize,
}

impl Matrix {
    fn new(rows: usize, cols: usize) -> Self {
        Matrix {
            data: vec![vec![0; cols]; rows],
            rows,
            cols,
        }
    }
    
    fn from_data(data: Vec<Vec<i32>>) -> Self {
        let rows = data.len();
        let cols = data[0].len();
        Matrix { data, rows, cols }
    }
}

// Addition
impl Add for Matrix {
    type Output = Matrix;
    
    fn add(self, other: Matrix) -> Matrix {
        assert_eq!(self.rows, other.rows);
        assert_eq!(self.cols, other.cols);
        
        let mut result = Matrix::new(self.rows, self.cols);
        for i in 0..self.rows {
            for j in 0..self.cols {
                result.data[i][j] = self.data[i][j] + other.data[i][j];
            }
        }
        result
    }
}

// Scalar multiplication
impl Mul<i32> for Matrix {
    type Output = Matrix;
    
    fn mul(self, scalar: i32) -> Matrix {
        let mut result = self.clone();
        for i in 0..self.rows {
            for j in 0..self.cols {
                result.data[i][j] *= scalar;
            }
        }
        result
    }
}

// Indexing
impl Index<usize> for Matrix {
    type Output = Vec<i32>;
    
    fn index(&self, index: usize) -> &Self::Output {
        &self.data[index]
    }
}

impl IndexMut<usize> for Matrix {
    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
        &mut self.data[index]
    }
}

fn main() {
    let mut m1 = Matrix::from_data(vec![
        vec![1, 2],
        vec![3, 4],
    ]);
    
    let m2 = Matrix::from_data(vec![
        vec![5, 6],
        vec![7, 8],
    ]);
    
    // Using overloaded operators
    let m3 = m1.clone() + m2;  // Addition
    println!("Addition result: {:?}", m3);
    
    let m4 = m1.clone() * 3;   // Scalar multiplication
    println!("Scalar multiplication: {:?}", m4);
    
    // Using indexing
    println!("m1[0]: {:?}", m1[0]);
    m1[0][0] = 99;  // Mutable indexing
    println!("After modification: {:?}", m1);
}

Expected Output:

Addition result: Matrix { data: [[6, 8], [10, 12]], rows: 2, cols: 2 }
Scalar multiplication: Matrix { data: [[3, 6], [9, 12]], rows: 2, cols: 2 }
m1[0]: [1, 2]
After modification: Matrix { data: [[99, 2], [3, 4]], rows: 2, cols: 2 }

Method Name Disambiguation

When multiple traits define methods with the same name, you need disambiguation:

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("Flying a plane");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Flying with magic");
    }
}

impl Human {
    fn fly(&self) {
        println!("Waving arms furiously");
    }
}

// Trait with associated function (no self)
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    let person = Human;
    
    // Different ways to call methods
    person.fly();                    // Calls Human::fly (inherent method)
    Pilot::fly(&person);            // Explicit trait method call
    Wizard::fly(&person);           // Explicit trait method call
    
    // Method syntax disambiguation
    <Human as Pilot>::fly(&person);
    <Human as Wizard>::fly(&person);
    
    println!();
    
    // Associated function disambiguation
    println!("Dog name: {}", Dog::baby_name());                    // Inherent
    println!("Animal name: {}", <Dog as Animal>::baby_name());     // Trait
}

Expected Output:

Waving arms furiously
Flying a plane
Flying with magic
Flying a plane
Flying with magic

Dog name: Spot
Animal name: puppy

Advanced Trait Bounds

use std::fmt::{Debug, Display};

// Complex trait bounds
fn complex_function<T>(item: T) 
where
    T: Display + Debug + Clone + PartialEq,
{
    println!("Display: {}", item);
    println!("Debug: {:?}", item);
    let cloned = item.clone();
    println!("Clone equal to original: {}", item == cloned);
}

// Trait bounds with associated types
fn process_iterator<I>(mut iter: I) 
where
    I: Iterator,
    I::Item: Display + Debug,
{
    while let Some(item) = iter.next() {
        println!("Item - Display: {}, Debug: {:?}", item, item);
    }
}

// Higher-ranked trait bounds (lifetime polymorphism)
fn higher_ranked<F>(f: F) 
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    let result = f("Hello");
    println!("Result: {}", result);
}

fn main() {
    complex_function(42);
    println!();
    
    let numbers = vec![1, 2, 3];
    process_iterator(numbers.into_iter());
    println!();
    
    higher_ranked(|s| &s[..2]);  // Takes first 2 characters
}

Expected Output:

Display: 42
Debug: 42
Clone equal to original: true

Item - Display: 1, Debug: 1
Item - Display: 2, Debug: 2
Item - Display: 3, Debug: 3

Result: He

Blanket Implementations

Implement a trait for all types that satisfy certain bounds:

use std::fmt::Display;

// Blanket implementation: implement ToString for all types that implement Display
// (This is actually how it's done in the standard library)
trait MyToString {
    fn my_to_string(&self) -> String;
}

impl<T: Display> MyToString for T {
    fn my_to_string(&self) -> String {
        format!("{}", self)
    }
}

// Now any type implementing Display automatically gets MyToString
struct Point {
    x: i32,
    y: i32,
}

impl Display for Point {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let point = Point { x: 3, y: 4 };
    let number = 42;
    
    // Both types automatically have MyToString because they implement Display
    println!("Point as string: {}", point.my_to_string());
    println!("Number as string: {}", number.my_to_string());
}

Expected Output:

Point as string: (3, 4)
Number as string: 42

Common Patterns and Best Practices

1. Builder Pattern with Traits

trait Builder {
    type Output;
    fn build(self) -> Self::Output;
}

struct ConfigBuilder {
    host: Option<String>,
    port: Option<u16>,
    timeout: Option<u64>,
}

impl ConfigBuilder {
    fn new() -> Self {
        ConfigBuilder {
            host: None,
            port: None,
            timeout: None,
        }
    }
    
    fn host(mut self, host: &str) -> Self {
        self.host = Some(host.to_string());
        self
    }
    
    fn port(mut self, port: u16) -> Self {
        self.port = Some(port);
        self
    }
    
    fn timeout(mut self, timeout: u64) -> Self {
        self.timeout = Some(timeout);
        self
    }
}

struct Config {
    host: String,
    port: u16,
    timeout: u64,
}

impl Builder for ConfigBuilder {
    type Output = Config;
    
    fn build(self) -> Self::Output {
        Config {
            host: self.host.unwrap_or_else(|| "localhost".to_string()),
            port: self.port.unwrap_or(8080),
            timeout: self.timeout.unwrap_or(30),
        }
    }
}

fn main() {
    let config = ConfigBuilder::new()
        .host("example.com")
        .port(3000)
        .build();
        
    println!("Config: {}:{} (timeout: {}s)", 
             config.host, config.port, config.timeout);
}

Expected Output:

Config: example.com:3000 (timeout: 30s)

Summary

Advanced trait features enable powerful, flexible designs:

These advanced features allow you to create sophisticated, type-safe APIs that are both powerful and ergonomic to use.


← PreviousNext →