1. rust
  2. /advanced
  3. /traits

Traits

Traits define shared behavior that types can implement. They're similar to interfaces in other languages but more powerful, enabling zero-cost abstractions and flexible code organization. Traits are fundamental to Rust's type system and enable many advanced patterns.

Basic Trait Definition

A trait defines a set of methods that types can implement:

trait Drawable {
    fn draw(&self);
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle with radius {}", self.radius);
    }
    
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

impl Drawable for Rectangle {
    fn draw(&self) {
        println!("Drawing a rectangle {}x{}", self.width, self.height);
    }
    
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

fn main() {
    let circle = Circle { radius: 5.0 };
    let rectangle = Rectangle { width: 3.0, height: 4.0 };
    
    circle.draw();
    println!("Circle area: {}", circle.area());
    
    rectangle.draw();
    println!("Rectangle area: {}", rectangle.area());
}

Default Implementations

Traits can provide default implementations for methods:

trait Greet {
    fn name(&self) -> &str;
    
    // Default implementation
    fn greet(&self) {
        println!("Hello, my name is {}", self.name());
    }
    
    // Another default implementation
    fn formal_greet(&self) {
        println!("Good day. I am {}", self.name());
    }
}

struct Person {
    name: String,
}

struct Robot {
    model: String,
}

impl Greet for Person {
    fn name(&self) -> &str {
        &self.name
    }
    
    // Override default implementation
    fn greet(&self) {
        println!("Hi there! I'm {}", self.name());
    }
}

impl Greet for Robot {
    fn name(&self) -> &str {
        &self.model
    }
    
    // Use default implementations for greet() and formal_greet()
}

fn main() {
    let person = Person { name: "Alice".to_string() };
    let robot = Robot { model: "R2D2".to_string() };
    
    person.greet();        // Uses overridden implementation
    person.formal_greet(); // Uses default implementation
    
    robot.greet();         // Uses default implementation
    robot.formal_greet();  // Uses default implementation
}

Trait Bounds

Use trait bounds to specify that generic types must implement certain traits:

trait Printable {
    fn print(&self);
}

struct Document {
    content: String,
}

struct Image {
    filename: String,
}

impl Printable for Document {
    fn print(&self) {
        println!("Printing document: {}", self.content);
    }
}

impl Printable for Image {
    fn print(&self) {
        println!("Printing image: {}", self.filename);
    }
}

// Function with trait bound
fn print_item<T: Printable>(item: &T) {
    item.print();
}

// Alternative syntax
fn print_item_alt<T>(item: &T) 
where 
    T: Printable,
{
    item.print();
}

// Multiple trait bounds
fn process_item<T>(item: &T) 
where 
    T: Printable + std::fmt::Debug,
{
    println!("Processing: {:?}", item);
    item.print();
}

fn main() {
    let doc = Document { content: "Hello, world!".to_string() };
    let img = Image { filename: "photo.jpg".to_string() };
    
    print_item(&doc);
    print_item(&img);
}

Associated Types

Associated types allow traits to define placeholder types:

trait Iterator {
    type Item;  // Associated type
    
    fn next(&mut self) -> Option<Self::Item>;
}

struct Counter {
    current: usize,
    max: usize,
}

impl Counter {
    fn new(max: usize) -> Counter {
        Counter { current: 0, max }
    }
}

impl Iterator for Counter {
    type Item = usize;  // Specify the associated type
    
    fn next(&mut self) -> Option<Self::Item> {
        if self.current < self.max {
            let current = self.current;
            self.current += 1;
            Some(current)
        } else {
            None
        }
    }
}

// Generic function using associated type
fn collect_items<I: Iterator>(mut iter: I) -> Vec<I::Item> {
    let mut items = Vec::new();
    while let Some(item) = iter.next() {
        items.push(item);
    }
    items
}

fn main() {
    let mut counter = Counter::new(5);
    let items = collect_items(counter);
    println!("Items: {:?}", items);
}

Trait Objects

Use trait objects for dynamic dispatch:

trait Animal {
    fn make_sound(&self);
    fn name(&self) -> &str;
}

struct Dog {
    name: String,
}

struct Cat {
    name: String,
}

impl Animal for Dog {
    fn make_sound(&self) {
        println!("Woof!");
    }
    
    fn name(&self) -> &str {
        &self.name
    }
}

impl Animal for Cat {
    fn make_sound(&self) {
        println!("Meow!");
    }
    
    fn name(&self) -> &str {
        &self.name
    }
}

// Function accepting trait objects
fn animal_sounds(animals: &[Box<dyn Animal>]) {
    for animal in animals {
        println!("{} says:", animal.name());
        animal.make_sound();
    }
}

fn main() {
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog { name: "Buddy".to_string() }),
        Box::new(Cat { name: "Whiskers".to_string() }),
        Box::new(Dog { name: "Max".to_string() }),
    ];
    
    animal_sounds(&animals);
}

Derived Traits

Rust can automatically implement common traits:

#[derive(Debug, Clone, PartialEq, Eq)]
struct Point {
    x: i32,
    y: i32,
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Person {
    name: String,
    age: u32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 1, y: 2 };
    let p3 = p1.clone();
    
    // Debug
    println!("Point: {:?}", p1);
    
    // PartialEq
    println!("p1 == p2: {}", p1 == p2);
    println!("p1 == p3: {}", p1 == p3);
    
    let alice = Person { name: "Alice".to_string(), age: 30 };
    let bob = Person { name: "Bob".to_string(), age: 25 };
    
    // Ordering (lexicographic by fields)
    println!("alice < bob: {}", alice < bob);
}

Standard Library Traits

Display and Debug

use std::fmt;

struct Temperature {
    celsius: f64,
}

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}°C", self.celsius)
    }
}

impl fmt::Debug for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Temperature {{ celsius: {} }}", self.celsius)
    }
}

fn main() {
    let temp = Temperature { celsius: 25.0 };
    
    println!("Display: {}", temp);      // Uses Display trait
    println!("Debug: {:?}", temp);      // Uses Debug trait
    println!("Pretty Debug: {:#?}", temp);
}

From and Into

struct Celsius(f64);
struct Fahrenheit(f64);

impl From<Fahrenheit> for Celsius {
    fn from(f: Fahrenheit) -> Self {
        Celsius((f.0 - 32.0) * 5.0 / 9.0)
    }
}

// Into is automatically implemented when From is implemented
impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

fn main() {
    let freezing_f = Fahrenheit(32.0);
    let freezing_c: Celsius = freezing_f.into();
    println!("32°F = {}°C", freezing_c.0);
    
    let boiling_c = Celsius(100.0);
    let boiling_f = Fahrenheit::from(boiling_c);
    println!("100°C = {}°F", boiling_f.0);
}

TryFrom and TryInto

use std::convert::TryFrom;
use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct PositiveInteger(u32);

#[derive(Debug)]
struct PositiveIntegerError;

impl fmt::Display for PositiveIntegerError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Value must be positive")
    }
}

impl Error for PositiveIntegerError {}

impl TryFrom<i32> for PositiveInteger {
    type Error = PositiveIntegerError;
    
    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value > 0 {
            Ok(PositiveInteger(value as u32))
        } else {
            Err(PositiveIntegerError)
        }
    }
}

fn main() {
    // Successful conversion
    match PositiveInteger::try_from(42) {
        Ok(pos_int) => println!("Created: {:?}", pos_int),
        Err(e) => println!("Error: {}", e),
    }
    
    // Failed conversion
    match PositiveInteger::try_from(-5) {
        Ok(pos_int) => println!("Created: {:?}", pos_int),
        Err(e) => println!("Error: {}", e),
    }
}

Advanced Trait Patterns

Supertraits

trait Animal {
    fn name(&self) -> &str;
}

// Mammal is a supertrait of Animal
trait Mammal: Animal {
    fn fur_color(&self) -> &str;
}

struct Dog {
    name: String,
    fur_color: String,
}

impl Animal for Dog {
    fn name(&self) -> &str {
        &self.name
    }
}

impl Mammal for Dog {
    fn fur_color(&self) -> &str {
        &self.fur_color
    }
}

fn describe_mammal<T: Mammal>(mammal: &T) {
    println!("{} has {} fur", mammal.name(), mammal.fur_color());
}

fn main() {
    let dog = Dog {
        name: "Buddy".to_string(),
        fur_color: "brown".to_string(),
    };
    
    describe_mammal(&dog);
}

Associated Constants

trait MathConstants {
    const PI: f64;
    const E: f64;
}

struct Calculator;

impl MathConstants for Calculator {
    const PI: f64 = 3.14159265359;
    const E: f64 = 2.71828182846;
}

trait Shape {
    const SIDES: u32;
    
    fn area(&self) -> f64;
}

struct Triangle {
    base: f64,
    height: f64,
}

impl Shape for Triangle {
    const SIDES: u32 = 3;
    
    fn area(&self) -> f64 {
        0.5 * self.base * self.height
    }
}

fn main() {
    println!("PI = {}", Calculator::PI);
    println!("E = {}", Calculator::E);
    
    let triangle = Triangle { base: 10.0, height: 5.0 };
    println!("Triangle has {} sides", Triangle::SIDES);
    println!("Triangle area: {}", triangle.area());
}

Generic Traits

trait Add<RHS = Self> {
    type Output;
    
    fn add(self, rhs: RHS) -> Self::Output;
}

#[derive(Debug, PartialEq)]
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,
        }
    }
}

// Different implementation for adding integers to points
impl Add<i32> for Point {
    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 };
    
    let p3 = p1.add(p2);
    println!("Point + Point: {:?}", p3);
    
    let p4 = Point { x: 1, y: 2 };
    let p5 = p4.add(5);
    println!("Point + i32: {:?}", p5);
}

Conditional Trait Implementation

trait Summary {
    fn summarize(&self) -> String;
}

// Implement Summary for any type that implements Display
impl<T: std::fmt::Display> Summary for T {
    fn summarize(&self) -> String {
        format!("Summary: {}", self)
    }
}

fn main() {
    let number = 42;
    let text = "Hello, world!";
    
    // These work because i32 and &str implement Display
    println!("{}", number.summarize());
    println!("{}", text.summarize());
}

Marker Traits

// Marker trait - no methods
trait Serializable {}

struct User {
    name: String,
    email: String,
}

struct Password {
    hash: String,
}

// Only User implements Serializable
impl Serializable for User {}

fn serialize<T: Serializable>(item: &T) -> String {
    // In real implementation, would serialize the item
    "serialized data".to_string()
}

fn main() {
    let user = User {
        name: "Alice".to_string(),
        email: "[email protected]".to_string(),
    };
    
    let _serialized = serialize(&user);  // This works
    
    let password = Password {
        hash: "secret_hash".to_string(),
    };
    
    // This would not compile:
    // let serialized = serialize(&password);
}

Trait Bounds in Practice

Where Clauses

use std::fmt::Debug;
use std::cmp::PartialOrd;

// Complex trait bounds are clearer with where clauses
fn compare_and_display<T, U>(t: &T, u: &U) -> std::cmp::Ordering
where
    T: Debug + PartialOrd<U>,
    U: Debug,
{
    println!("Comparing {:?} with {:?}", t, u);
    t.partial_cmp(u).unwrap()
}

fn main() {
    let result = compare_and_display(&5, &3);
    println!("Result: {:?}", result);
}

Higher-Ranked Trait Bounds

fn call_with_ref<F>(f: F) 
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    let s = "hello";
    let result = f(s);
    println!("Result: {}", result);
}

fn identity(s: &str) -> &str {
    s
}

fn main() {
    call_with_ref(identity);
    call_with_ref(|s| s);
}

Best Practices

1. Use Descriptive Trait Names

// Good: Clear purpose
trait Drawable {
    fn draw(&self);
}

trait Comparable {
    fn compare(&self, other: &Self) -> std::cmp::Ordering;
}

// Avoid: Vague names
trait Process {
    fn do_thing(&self);
}

2. Keep Traits Focused

// Good: Single responsibility
trait Readable {
    fn read(&self) -> String;
}

trait Writable {
    fn write(&mut self, data: &str);
}

// Avoid: Too many responsibilities
trait FileHandler {
    fn read(&self) -> String;
    fn write(&mut self, data: &str);
    fn compress(&self) -> Vec<u8>;
    fn encrypt(&self, key: &str) -> Vec<u8>;
    fn backup(&self, location: &str) -> bool;
}

3. Use Associated Types for Strong Coupling

// Good: Associated type when there's one logical choice
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

// Use generics when there could be multiple implementations
trait Add<RHS = Self> {
    type Output;
    fn add(self, rhs: RHS) -> Self::Output;
}

4. Provide Default Implementations When Sensible

trait Logger {
    fn log(&self, message: &str);
    
    // Default implementations for convenience
    fn info(&self, message: &str) {
        self.log(&format!("INFO: {}", message));
    }
    
    fn error(&self, message: &str) {
        self.log(&format!("ERROR: {}", message));
    }
}

5. Use Trait Objects Judiciously

// Good: When you need runtime polymorphism
fn process_shapes(shapes: &[Box<dyn Drawable>]) {
    for shape in shapes {
        shape.draw();
    }
}

// Better: When you can use generics
fn process_shapes_generic<T: Drawable>(shapes: &[T]) {
    for shape in shapes {
        shape.draw();
    }
}

Traits are central to Rust's type system and enable powerful abstractions while maintaining zero-cost performance. They provide a way to define shared behavior, implement polymorphism, and create flexible APIs that work with many different types. Understanding traits deeply is essential for writing idiomatic and effective Rust code.