11 / 20 · Day 4
Day 4 · Concept 11

Generics

Type parameters with trait bounds. Rust monomorphises: each instantiation generates specialised code, zero runtime cost. The cost is in binary size and compile time — but the resulting code is as fast as if you'd hand-written each version.


1 · Try it — generic function

rust src/main.rs
fn largest<T: PartialOrd>(items: &[T]) -> &T {
    let mut biggest = &items[0];
    for x in items {
        if x > biggest {
            biggest = x;
        }
    }
    biggest
}

fn main() {
    println!("{}", largest(&[1, 5, 3, 9, 2]));
    println!("{}", largest(&["go", "rust", "c"]));
}

2 · Multiple bounds, where clauses

rust src/main.rs · where reads cleaner
use std::fmt::Debug;
use std::hash::Hash;

// Inline bounds — short
fn one<T: Clone + Debug>(x: T) { println!("{:?}", x.clone()); }

// where clause — longer signatures
fn two<T, U>(x: T, y: U) -> String
where
    T: Debug + Hash,
    U: Debug + Clone,
{
    format!("{:?} {:?}", x, y.clone())
}

3 · Generic types — structs and enums

rust src/main.rs
struct Pair<T> {
    first: T,
    second: T,
}

impl<T: std::fmt::Display> Pair<T> {
    fn print(&self) {
        println!("{} {}", self.first, self.second);
    }
}

fn main() {
    let pi = Pair { first: 1, second: 2 };
    pi.print();
    let ps = Pair { first: "a", second: "b" };
    ps.print();
}

4 · Monomorphisation — what the compiler does

When you call largest::<i32>(...) and largest::<&str>(...), the compiler generates two versions of largest — one specialised for each type. Zero runtime overhead compared to writing them by hand.

The trade. Faster runtime; slower compile times; larger binaries. For most projects, fine. For huge libraries, generic-heavy code compiles slowly.

5 · When it clicks

  • You reach for generics when you'd duplicate a function for different types.
  • Trait bounds describe what your function needs from T, not what T is.
  • You use where clauses for anything over 3 bounds.
Found this useful?