DaleSchool

Generics: Types as Variables

Intermediate15min

Learning Objectives

  • Define and call generic functions
  • Define generic structs
  • Restrict generic types with trait bounds
  • Understand that Vec, Option, and Result are all generic

Working Code

You've been using Vec<i32>, Option<String>, and Result<String, io::Error> all along. That angle-bracket-with-a-type syntax? It's all generics!

First, let's see what happens without generics:

fn print_i32(value: i32) {
    println!("Value: {value}");
}

fn print_str(value: &str) {
    println!("Value: {value}");
}

fn print_f64(value: f64) {
    println!("Value: {value}");
}

fn main() {
    print_i32(42);
    print_str("hello");
    print_f64(3.14);
}

You need a separate function for each type. Ten types means ten functions!

Try It Yourself

  1. Add a print_bool(value: bool) function. Getting tedious, right?
  2. Compare with the generic version below — one function handles them all!

Generic Functions — Write Once, Use for Any Type

use std::fmt::Display;

fn print_value<T: Display>(value: T) {
    println!("Value: {value}");
}

fn main() {
    print_value(42);
    print_value("hello");
    print_value(3.14);
    print_value(true);
}

<T: Display> means "I'll use a type variable called T, but only types with the Display certificate." The trait bounds from Lesson 20 are in action!

T is a conventional name — short for Type. U, V, and others are also common.

Type Inference — Usually Omitted

Rust infers the type automatically. You can specify it explicitly, but you usually don't:

use std::fmt::Display;

fn print_value<T: Display>(value: T) {
    println!("Value: {value}");
}

fn main() {
    print_value::<i32>(42);    // explicit — usually unnecessary
    print_value(42);           // inferred — use this
}

Multiple Generic Types

You can use more than one generic type:

use std::fmt::Display;

fn print_pair<T: Display, U: Display>(first: T, second: U) {
    println!("{first}, {second}");
}

fn main() {
    print_pair(1, "apple");
    print_pair(3.14, 42);
}

Generic Structs

Structs can be generic too:

#[derive(Debug)]
struct Pair<T> {
    first: T,
    second: T,
}

impl<T> Pair<T> {
    fn new(first: T, second: T) -> Self {
        Pair { first, second }
    }
}

fn main() {
    let integers = Pair::new(1, 2);
    let strings = Pair::new("hello", "world");

    println!("{:?}", integers); // Pair { first: 1, second: 2 }
    println!("{:?}", strings);  // Pair { first: "hello", second: "world" }
}

In Pair<T>, first and second must be the same type T. Want different types?

#[derive(Debug)]
struct MixedPair<T, U> {
    first: T,
    second: U,
}

fn main() {
    let mix = MixedPair { first: 42, second: "hello" };
    println!("{:?}", mix);
}

Generics You've Already Used

You've been using generics all along!

fn main() {
    // Vec<T> — a vector holding any type
    let numbers: Vec<i32> = vec![1, 2, 3];
    let names: Vec<&str> = vec!["Alice", "Bob"];

    // Option<T> — a value that may or may not exist
    let some_number: Option<i32> = Some(42);
    let no_number: Option<i32> = None;

    // Result<T, E> — success or failure
    let ok: Result<i32, String> = Ok(42);
    let err: Result<i32, String> = Err("error!".into());

    println!("{:?}, {:?}", numbers, names);
    println!("{:?}, {:?}", some_number, no_number);
    println!("{:?}, {:?}", ok, err);
}

Vec, Option, and Result are all generic types. The i32, String, etc. inside angle brackets are the type arguments.

where Clause — Cleaner Trait Bounds

When trait bounds get long, move them to a where clause:

Inline bounds get crowded

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

fn compare_and_print<T: Display + PartialOrd + Debug>(a: T, b: T) {
    if a > b {
        println!("{a} > {b}");
    } else {
        println!("{a} <= {b}");
    }
}

where clause is cleaner

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

fn compare_and_print<T>(a: T, b: T)
where
    T: Display + PartialOrd + Debug,
{
    if a > b {
        println!("{a} > {b}");
    } else {
        println!("{a} <= {b}");
    }
}

fn main() {
    compare_and_print(10, 20);
    compare_and_print("apple", "banana");
}

Functionally identical. where is purely a readability tool.

Learn More: Monomorphization

"Don't generics make code slower?" Don't worry! Rust uses monomorphization.

At compile time, the compiler creates a concrete copy of the generic function for each type actually used:

fn print_value<T: std::fmt::Display>(value: T) {
    println!("Value: {value}");
}

fn main() {
    print_value(42);     // an i32 version is generated
    print_value("hello"); // a &str version is generated
}

The compiler internally produces something like:

fn print_value_i32(value: i32) {
    println!("Value: {value}");
}

fn print_value_str(value: &str) {
    println!("Value: {value}");
}

Generics reduce code duplication with zero runtime cost.

Exercise 1 (easy). Complete the generic function. It should return the larger of two values.

fn max_of<T: PartialOrd>(a: T, b: T) -> T {
    // Complete this
}

fn main() {
    println!("{}", max_of(10, 20));           // 20
    println!("{}", max_of("apple", "banana")); // banana
}

Exercise 2 (medium). Implement a generic Stack<T> struct.

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

impl<T> Stack<T> {
    fn new() -> Self {
        // Complete this
    }

    fn push(&mut self, item: T) {
        // Complete this
    }

    fn pop(&mut self) -> Option<T> {
        // Complete this
        // Hint: use Vec's pop() method
    }

    fn is_empty(&self) -> bool {
        // Complete this
    }
}

fn main() {
    let mut stack = Stack::new();
    stack.push(1);
    stack.push(2);
    stack.push(3);

    while let Some(value) = stack.pop() {
        println!("{value}"); // prints 3, 2, 1
    }
}

Exercise 3 (challenge). Write a generic function with a where clause that requires Display + Clone. It should clone the value and print both.

use std::fmt::Display;

fn print_cloned<T>(value: T)
where
    // Add bounds here
{
    let cloned = value.clone();
    println!("Original: {value}");
    println!("Cloned: {cloned}");
}

fn main() {
    print_cloned(String::from("Rust"));
    print_cloned(42);
}

Q1. What is the main purpose of generics?

  • A) Making programs faster
  • B) Writing one piece of code that works with many types
  • C) Reducing memory usage
  • D) Automating error handling

Q2. Which is equivalent to fn foo<T: Display + Clone>(x: T)?

  • A) fn foo<T>(x: T) where T: Display
  • B) fn foo<T>(x: T) where T: Display + Clone
  • C) fn foo<T>(x: T) where T: Clone
  • D) fn foo(x: Display + Clone)

Q3. How do Rust generics affect runtime performance?

  • A) Slightly slower
  • B) Zero cost (monomorphization converts to concrete types at compile time)
  • C) Doubles memory usage
  • D) Requires garbage collection