BBuzzard

A type parameter in angle brackets, <T>, stands for a type the caller chooses. Functions, structs, enums, and impls can all be generic. The compiler monomorphizes: each concrete type used gets its own compiled copy, so generic code is as fast as hand-written code.

Without a bound, a T is opaque: you can move it and drop it, nothing more. A bound such as T: Clone + Debug says what T must support, and unlocks those methods. Bounds can go inline, or in a where clause when they get long. Turbofish ::<T> names the type explicitly when inference cannot.

Generic functions and turbofish

fn first<T: Clone>(items: &[T]) -> Option<T> {
    items.first().cloned()
}

fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
    let mut best = items[0];
    for &i in items {
        if i > best {
            best = i;
        }
    }

    best
}

fn demo_functions() {
    println!("-- generic functions --");

    println!("{:?} {:?}", first(&["a", "b"]), first::<i32>(&[]));
    println!(
        "largest {} {} {}",
        largest(&[3, 7, 2]),
        largest(&[1.5, 0.2]),
        largest(&['x', 'b'])
    );

    let parsed = "5".parse::<u8>();
    let collected = (1..4).collect::<Vec<_>>();
    println!("{parsed:?} {collected:?}");

    // fn bad<T>(a: T, b: T) -> bool { a == b }
    // error[E0369]: binary operation `==` cannot be applied to type `T`
}

Generic structs and impls

#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

impl<T: Display> Point<T> {
    fn show(&self) {
        println!("({}, {})", self.x, self.y);
    }
}

// Methods only for one concrete type.
impl Point<f64> {
    fn dist(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

#[derive(Debug)]
struct Pair<A, B> {
    a: A,
    b: B,
}

impl<A, B> Pair<A, B> {
    fn swap(self) -> Pair<B, A> {
        Pair { a: self.b, b: self.a }
    }
}

fn demo_structs() {
    println!("-- generic structs --");

    Point { x: 1, y: 2 }.show();
    Point { x: "left", y: "right" }.show();

    let p = Point { x: 3.0, y: 4.0 };
    println!("dist {}", p.dist());

    // Point { x: 3, y: 4 }.dist(); // error[E0599]: no method named `dist` for Point<i32>
    // let mixed = Point { x: 1, y: 2.0 }; // error[E0308]: mismatched types
    let pair = Pair { a: 1, b: "one" };
    println!("{pair:?}");

    let swapped = pair.swap(); // swap takes self, so pair is moved here
    println!("swapped {swapped:?} a={} b={}", swapped.a, swapped.b);
}

Generic enums, like Option and Result

#[derive(Debug)]
enum Either<L, R> {
    Left(L),
    Right(R),
}

impl<L: Debug, R: Debug> Either<L, R> {
    fn describe(&self) -> String {
        match self {
            Either::Left(l) => format!("left {l:?}"),
            Either::Right(r) => format!("right {r:?}"),
        }
    }
}

fn parse_or_keep(s: &str) -> Either<i32, &str> {
    s.parse().map(Either::Left).unwrap_or(Either::Right(s))
}

fn demo_enums() {
    println!("-- generic enums --");

    for s in ["12", "twelve"] {
        println!("{}", parse_or_keep(s).describe());
    }

    let items: Vec<Either<i32, &str>> = vec![Either::Left(1), Either::Right("x")];
    println!("{items:?}");
}

where clauses with several bounds

fn dump<T>(value: T)
where
    T: Debug + Clone,
{
    let copy = value.clone();
    println!("dump {copy:?}");
}

fn count_by<T, K, F>(items: &[T], key: F) -> HashMap<K, usize>
where
    K: Eq + Hash,
    F: Fn(&T) -> K,
{
    let mut out = HashMap::new();
    for i in items {
        *out.entry(key(i)).or_insert(0) += 1;
    }

    out
}

fn demo_where() {
    println!("-- where clauses --");

    dump(vec![1, 2, 3]);
    dump("text");

    let words = ["apple", "avocado", "banana", "blueberry", "cherry"];
    let mut counts: Vec<_> =
        count_by(&words, |w| w.chars().next().unwrap()).into_iter().collect();
    counts.sort();
    println!("by first letter {counts:?}");
}

A generic container with conditional methods

struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    fn new() -> Self {
        Stack { items: Vec::new() }
    }

    fn push(&mut self, t: T) {
        self.items.push(t);
    }

    fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }

    fn peek(&self) -> Option<&T> {
        self.items.last()
    }
}

// Extra methods only when T can be printed.
impl<T: Display> Stack<T> {
    fn render(&self) -> String {
        self.items.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(" | ")
    }
}

fn demo_container() {
    println!("-- generic container --");

    let mut s = Stack::new();
    s.push(1);
    s.push(2);
    s.push(3);
    println!("render {} peek {:?}", s.render(), s.peek());
    println!("pop {:?} now {}", s.pop(), s.render());

    let mut names: Stack<String> = Stack::new();
    names.push("ada".into());
    println!("{}", names.render());
}

Monomorphization is visible through type_name

fn type_of<T>(_: &T) -> &'static str {
    std::any::type_name::<T>()
}
$ cargo run --bin 25_generics
-- generic functions --
Some("a") None
largest 7 1.5 x
Ok(5) [1, 2, 3]
-- generic structs --
(1, 2)
(left, right)
dist 5
Pair { a: 1, b: "one" }
swapped Pair { a: "one", b: 1 } a=one b=1
-- generic enums --
left 12
right "twelve"
[Left(1), Right("x")]
-- where clauses --
dump [1, 2, 3]
dump "text"
by first letter [('a', 2), ('b', 2), ('c', 1)]
-- generic container --
render 1 | 2 | 3 peek Some(3)
pop Some(3) now 1 | 2
ada
-- monomorphization --
u8 &str alloc::vec::Vec<core::option::Option<f64>>
Point<u8> 2 bytes, Point<f64> 16 bytes

Full program

25_generics/main.rs · 238 lines
View on GitHub
// 25. Generics and trait bounds
//
// A type parameter in angle brackets, <T>, stands for a type the caller chooses.
// Functions, structs, enums, and impls can all be generic. The compiler monomorphizes:
// each concrete type used gets its own compiled copy, so generic code is as fast as
// hand-written code.
//
// Without a bound, a T is opaque: you can move it and drop it, nothing more. A bound
// such as T: Clone + Debug says what T must support, and unlocks those methods. Bounds
// can go inline, or in a where clause when they get long. Turbofish ::<T> names the
// type explicitly when inference cannot.
//
// Run: cargo run --bin 25_generics

use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::hash::Hash;

fn first<T: Clone>(items: &[T]) -> Option<T> {
    items.first().cloned()
}

fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
    let mut best = items[0];
    for &i in items {
        if i > best {
            best = i;
        }
    }

    best
}

// Example 1: generic functions and turbofish.
fn demo_functions() {
    println!("-- generic functions --");

    println!("{:?} {:?}", first(&["a", "b"]), first::<i32>(&[]));
    println!(
        "largest {} {} {}",
        largest(&[3, 7, 2]),
        largest(&[1.5, 0.2]),
        largest(&['x', 'b'])
    );

    let parsed = "5".parse::<u8>();
    let collected = (1..4).collect::<Vec<_>>();
    println!("{parsed:?} {collected:?}");

    // fn bad<T>(a: T, b: T) -> bool { a == b }
    // error[E0369]: binary operation `==` cannot be applied to type `T`
}

#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

impl<T: Display> Point<T> {
    fn show(&self) {
        println!("({}, {})", self.x, self.y);
    }
}

// Methods only for one concrete type.
impl Point<f64> {
    fn dist(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

#[derive(Debug)]
struct Pair<A, B> {
    a: A,
    b: B,
}

impl<A, B> Pair<A, B> {
    fn swap(self) -> Pair<B, A> {
        Pair { a: self.b, b: self.a }
    }
}

// Example 2: generic structs and impls.
fn demo_structs() {
    println!("-- generic structs --");

    Point { x: 1, y: 2 }.show();
    Point { x: "left", y: "right" }.show();

    let p = Point { x: 3.0, y: 4.0 };
    println!("dist {}", p.dist());

    // Point { x: 3, y: 4 }.dist(); // error[E0599]: no method named `dist` for Point<i32>
    // let mixed = Point { x: 1, y: 2.0 }; // error[E0308]: mismatched types
    let pair = Pair { a: 1, b: "one" };
    println!("{pair:?}");

    let swapped = pair.swap(); // swap takes self, so pair is moved here
    println!("swapped {swapped:?} a={} b={}", swapped.a, swapped.b);
}

#[derive(Debug)]
enum Either<L, R> {
    Left(L),
    Right(R),
}

impl<L: Debug, R: Debug> Either<L, R> {
    fn describe(&self) -> String {
        match self {
            Either::Left(l) => format!("left {l:?}"),
            Either::Right(r) => format!("right {r:?}"),
        }
    }
}

fn parse_or_keep(s: &str) -> Either<i32, &str> {
    s.parse().map(Either::Left).unwrap_or(Either::Right(s))
}

// Example 3: generic enums, like Option and Result.
fn demo_enums() {
    println!("-- generic enums --");

    for s in ["12", "twelve"] {
        println!("{}", parse_or_keep(s).describe());
    }

    let items: Vec<Either<i32, &str>> = vec![Either::Left(1), Either::Right("x")];
    println!("{items:?}");
}

fn dump<T>(value: T)
where
    T: Debug + Clone,
{
    let copy = value.clone();
    println!("dump {copy:?}");
}

fn count_by<T, K, F>(items: &[T], key: F) -> HashMap<K, usize>
where
    K: Eq + Hash,
    F: Fn(&T) -> K,
{
    let mut out = HashMap::new();
    for i in items {
        *out.entry(key(i)).or_insert(0) += 1;
    }

    out
}

// Example 4: where clauses with several bounds.
fn demo_where() {
    println!("-- where clauses --");

    dump(vec![1, 2, 3]);
    dump("text");

    let words = ["apple", "avocado", "banana", "blueberry", "cherry"];
    let mut counts: Vec<_> =
        count_by(&words, |w| w.chars().next().unwrap()).into_iter().collect();
    counts.sort();
    println!("by first letter {counts:?}");
}

struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    fn new() -> Self {
        Stack { items: Vec::new() }
    }

    fn push(&mut self, t: T) {
        self.items.push(t);
    }

    fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }

    fn peek(&self) -> Option<&T> {
        self.items.last()
    }
}

// Extra methods only when T can be printed.
impl<T: Display> Stack<T> {
    fn render(&self) -> String {
        self.items.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(" | ")
    }
}

// Example 5: a generic container with conditional methods.
fn demo_container() {
    println!("-- generic container --");

    let mut s = Stack::new();
    s.push(1);
    s.push(2);
    s.push(3);
    println!("render {} peek {:?}", s.render(), s.peek());
    println!("pop {:?} now {}", s.pop(), s.render());

    let mut names: Stack<String> = Stack::new();
    names.push("ada".into());
    println!("{}", names.render());
}

// Example 6: monomorphization is visible through type_name.
fn type_of<T>(_: &T) -> &'static str {
    std::any::type_name::<T>()
}

fn demo_monomorphization() {
    println!("-- monomorphization --");

    println!("{} {} {}", type_of(&1u8), type_of(&"s"), type_of(&vec![Some(1.0)]));
    println!(
        "Point<u8> {} bytes, Point<f64> {} bytes",
        size_of::<Point<u8>>(),
        size_of::<Point<f64>>()
    );
}

fn main() {
    demo_functions();
    demo_structs();
    demo_enums();
    demo_where();
    demo_container();
    demo_monomorphization();
}