Associated Types and Advanced Traits

Default Generic Type Parameters

A default generic type parameter specifies a fallback type (<T = DefaultType>) in a trait, struct, enum, or type alias declaration so users can omit the type argument in the common case while retaining the ability to override it.

Default Type Parameters on Traits (<Rhs = Self>)

Binary operator and comparison traits in the standard library (Add<Rhs = Self>, Sub<Rhs = Self>, PartialEq<Rhs = Self>) set their right-hand side type parameter to Self by default:

pub trait Add<Rhs = Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}
  • Writing impl Add for Point uses the default Rhs = Point (adding two Point values).
  • Writing impl Add<i32> for Point overrides Rhs to i32 (adding a scalar offset to a Point).
trait Combine<Rhs = Self> {
    type Output;
    fn combine(self, rhs: Rhs) -> Self::Output;
}

#[derive(Debug, Clone, Copy)]
struct Vec2 {
    x: f64,
    y: f64,
}

// 1. Uses the default parameter `Rhs = Self` (Vec2 + Vec2):
impl Combine for Vec2 {
    type Output = Vec2;

    fn combine(self, rhs: Vec2) -> Vec2 {
        Vec2 {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

// 2. Overrides `Rhs` with `f64` (Vec2 + f64 uniform scale):
impl Combine<f64> for Vec2 {
    type Output = Vec2;

    fn combine(self, scale: f64) -> Vec2 {
        Vec2 {
            x: self.x * scale,
            y: self.y * scale,
        }
    }
}

fn main() {
    let a = Vec2 { x: 2.0, y: 3.0 };
    let b = Vec2 { x: 4.0, y: 1.0 };

    println!("Combine Vec2: {:?}", a.combine(b));
    println!("Combine f64:  {:?}", a.combine(10.0));
}
Combine Vec2: Vec2 { x: 6.0, y: 4.0 }
Combine f64:  Vec2 { x: 20.0, y: 30.0 }

Default Type Parameters on Structs and Collections

Standard library collections use default type parameters for hashers and allocators so callers can write HashMap<K, V> instead of spelling out RandomState every time:

// Standard library signature:
pub struct HashMap<K, V, S = RandomState> { /* ... */ }

You can use the same technique in custom containers or builders:

struct PlainFormatter;
struct UppercaseFormatter;

struct Logger<F = PlainFormatter> {
    prefix: String,
    formatter: F,
}

impl Logger<PlainFormatter> {
    fn new(prefix: &str) -> Self {
        Self {
            prefix: prefix.to_owned(),
            formatter: PlainFormatter,
        }
    }
}

impl<F> Logger<F> {
    fn with_formatter<NewF>(self, formatter: NewF) -> Logger<NewF> {
        let _ = &self.formatter;
        Logger {
            prefix: self.prefix,
            formatter,
        }
    }
}

fn main() {
    // Type annotation `Logger` uses `Logger<PlainFormatter>` by default:
    let default_log: Logger = Logger::new("APP");
    let custom_log: Logger<UppercaseFormatter> = default_log.with_formatter(UppercaseFormatter);
    let _ = &custom_log.formatter;
    println!("Configured logger with prefix: {}", custom_log.prefix);
}
Configured logger with prefix: APP

Ordering Rule for Default Type Parameters

Type parameters with default values must appear after all type parameters without default values (struct Foo<A, B = i32> is valid; struct Foo<A = i32, B> is a syntax error).