1. rust
  2. /advanced
  3. /generics

Generics

Generics allow you to write flexible, reusable code that works with many different types while maintaining compile-time type safety. They enable powerful abstractions without runtime overhead, making your code both generic and performant.

Generic Functions

Generic functions can work with multiple types by using type parameters:

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    
    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let result = largest(&number_list);
    println!("The largest number is {}", result);
    
    let char_list = vec!['y', 'm', 'a', 'q'];
    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

Multiple Type Parameters

Functions can have multiple generic type parameters:

fn pair<T, U>(first: T, second: U) -> (T, U) {
    (first, second)
}

fn swap<T, U>(pair: (T, U)) -> (U, T) {
    (pair.1, pair.0)
}

fn compare_types<T, U>(_x: T, _y: U) -> &'static str 
where
    T: std::fmt::Debug,
    U: std::fmt::Debug,
{
    "Received two debug-printable values"
}

fn main() {
    let p1 = pair(42, "hello");
    println!("Pair: {:?}", p1);
    
    let p2 = swap(p1);
    println!("Swapped: {:?}", p2);
    
    let result = compare_types(10, "world");
    println!("{}", result);
}

Generic Structs

Structs can be generic over one or more types:

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn new(x: T, y: T) -> Self {
        Point { x, y }
    }
    
    fn x(&self) -> &T {
        &self.x
    }
    
    fn y(&self) -> &T {
        &self.y
    }
}

// Methods for specific types
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let integer_point = Point::new(5, 10);
    let float_point = Point::new(1.0, 4.0);
    
    println!("Integer point: ({}, {})", integer_point.x(), integer_point.y());
    println!("Float point: ({}, {})", float_point.x(), float_point.y());
    println!("Distance from origin: {}", float_point.distance_from_origin());
}

Multiple Generic Parameters in Structs

struct Pair<T, U> {
    first: T,
    second: U,
}

impl<T, U> Pair<T, U> {
    fn new(first: T, second: U) -> Self {
        Pair { first, second }
    }
    
    fn into_tuple(self) -> (T, U) {
        (self.first, self.second)
    }
    
    fn as_refs(&self) -> (&T, &U) {
        (&self.first, &self.second)
    }
}

// Mixup method with different generic parameters
impl<T, U> Pair<T, U> {
    fn mixup<V, W>(self, other: Pair<V, W>) -> Pair<T, W> {
        Pair {
            first: self.first,
            second: other.second,
        }
    }
}

fn main() {
    let pair1 = Pair::new(5, "hello");
    let pair2 = Pair::new(true, 3.14);
    
    let mixed = pair1.mixup(pair2);
    println!("Mixed pair: {} and {}", mixed.first, mixed.second);
}

Generic Enums

Enums can also be generic:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

enum Option<T> {
    Some(T),
    None,
}

// Custom generic enum
#[derive(Debug)]
enum Container<T> {
    Empty,
    Single(T),
    Multiple(Vec<T>),
}

impl<T> Container<T> {
    fn new() -> Self {
        Container::Empty
    }
    
    fn add(self, item: T) -> Self {
        match self {
            Container::Empty => Container::Single(item),
            Container::Single(existing) => Container::Multiple(vec![existing, item]),
            Container::Multiple(mut items) => {
                items.push(item);
                Container::Multiple(items)
            }
        }
    }
    
    fn count(&self) -> usize {
        match self {
            Container::Empty => 0,
            Container::Single(_) => 1,
            Container::Multiple(items) => items.len(),
        }
    }
}

fn main() {
    let container = Container::new()
        .add("first")
        .add("second")
        .add("third");
    
    println!("Container: {:?}", container);
    println!("Count: {}", container.count());
}

Type Constraints and Bounds

Use trait bounds to constrain generic types:

use std::fmt::Display;
use std::cmp::PartialOrd;

// Simple trait bound
fn print_value<T: Display>(value: T) {
    println!("Value: {}", value);
}

// Multiple trait bounds
fn compare_and_display<T>(a: T, b: T) 
where
    T: Display + PartialOrd + Copy,
{
    println!("Comparing {} and {}", a, b);
    if a > b {
        println!("{} is greater", a);
    } else if a < b {
        println!("{} is greater", b);
    } else {
        println!("They are equal");
    }
}

// Conditional implementations
struct Wrapper<T> {
    value: T,
}

impl<T> Wrapper<T> {
    fn new(value: T) -> Self {
        Wrapper { value }
    }
}

// Only implement for types that can be displayed
impl<T: Display> Wrapper<T> {
    fn print(&self) {
        println!("Wrapped value: {}", self.value);
    }
}

// Only implement for numeric types
impl<T> Wrapper<T> 
where
    T: std::ops::Add<Output = T> + Copy,
{
    fn double(&self) -> T {
        self.value + self.value
    }
}

fn main() {
    print_value(42);
    print_value("hello");
    
    compare_and_display(10, 20);
    compare_and_display('a', 'z');
    
    let wrapped_num = Wrapper::new(42);
    wrapped_num.print();
    println!("Doubled: {}", wrapped_num.double());
    
    let wrapped_str = Wrapper::new("hello");
    wrapped_str.print();
    // wrapped_str.double(); // This wouldn't compile
}

Generic Implementations

You can implement traits generically:

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

// Generic implementation for all T
impl<T> Point<T> {
    fn new(x: T, y: T) -> Self {
        Point { x, y }
    }
}

// Implementation for specific types
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

// Implementation with constraints
impl<T> Point<T> 
where
    T: std::ops::Add<Output = T> + Copy,
{
    fn translate(&self, dx: T, dy: T) -> Point<T> {
        Point {
            x: self.x + dx,
            y: self.y + dy,
        }
    }
}

// Trait implementation for generic type
impl<T: std::fmt::Display> std::fmt::Display for Point<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let int_point = Point::new(3, 4);
    let float_point = Point::new(3.0, 4.0);
    
    println!("Integer point: {}", int_point);
    println!("Float point: {}", float_point);
    println!("Distance: {}", float_point.distance_from_origin());
    
    let translated = int_point.translate(1, 2);
    println!("Translated: {}", translated);
}

Associated Types vs Generic Parameters

Choose between associated types and generic parameters based on usage:

// Using generic parameters - can have multiple implementations
trait Add<RHS = Self> {
    type Output;
    fn add(self, rhs: RHS) -> Self::Output;
}

// Using associated types - one logical implementation per type
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

// Example: Multiple Add implementations
#[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,
        }
    }
}

impl Add<i32> for Point {
    type Output = Point;
    
    fn add(self, scalar: i32) -> Point {
        Point {
            x: self.x + scalar,
            y: self.y + scalar,
        }
    }
}

// Example: Iterator with associated type
struct Counter {
    current: usize,
    max: usize,
}

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

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

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 3, y: 4 };
    
    let p3 = p1.add(p2);              // Point + Point
    let p4 = Point { x: 1, y: 2 }.add(5); // Point + i32
    
    println!("Point + Point: {:?}", p3);
    println!("Point + i32: {:?}", p4);
    
    let mut counter = Counter::new(3);
    while let Some(value) = counter.next() {
        println!("Counter: {}", value);
    }
}

Lifetime Parameters with Generics

Combine generics with lifetime parameters:

struct Container<'a, T> {
    value: &'a T,
}

impl<'a, T> Container<'a, T> {
    fn new(value: &'a T) -> Self {
        Container { value }
    }
    
    fn get(&self) -> &T {
        self.value
    }
}

// Function with both lifetime and type parameters
fn longest_with_context<'a, T>(x: &'a str, y: &'a str, context: T) -> &'a str 
where
    T: std::fmt::Display,
{
    println!("Context: {}", context);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let number = 42;
    let container = Container::new(&number);
    println!("Container value: {}", container.get());
    
    let string1 = "hello";
    let string2 = "world!";
    let result = longest_with_context(string1, string2, "Comparing strings");
    println!("Longest: {}", result);
}

Advanced Generic Patterns

Higher-Kinded Types (Simulation)

trait Functor<A> {
    type Wrapped<B>;
    fn map<B, F>(self, f: F) -> Self::Wrapped<B>
    where
        F: FnOnce(A) -> B;
}

impl<A> Functor<A> for Option<A> {
    type Wrapped<B> = Option<B>;
    
    fn map<B, F>(self, f: F) -> Option<B>
    where
        F: FnOnce(A) -> B,
    {
        match self {
            Some(value) => Some(f(value)),
            None => None,
        }
    }
}

impl<A, E> Functor<A> for Result<A, E> {
    type Wrapped<B> = Result<B, E>;
    
    fn map<B, F>(self, f: F) -> Result<B, E>
    where
        F: FnOnce(A) -> B,
    {
        match self {
            Ok(value) => Ok(f(value)),
            Err(e) => Err(e),
        }
    }
}

fn main() {
    let opt = Some(42);
    let doubled = opt.map(|x| x * 2);
    println!("Doubled option: {:?}", doubled);
    
    let res: Result<i32, &str> = Ok(10);
    let tripled = res.map(|x| x * 3);
    println!("Tripled result: {:?}", tripled);
}

Generic Builders

struct Query<T> {
    data: T,
    limit: Option<usize>,
    offset: Option<usize>,
}

impl<T> Query<T> {
    fn new(data: T) -> Self {
        Query {
            data,
            limit: None,
            offset: None,
        }
    }
    
    fn limit(mut self, limit: usize) -> Self {
        self.limit = Some(limit);
        self
    }
    
    fn offset(mut self, offset: usize) -> Self {
        self.offset = Some(offset);
        self
    }
    
    fn execute(self) -> QueryResult<T> {
        QueryResult {
            data: self.data,
            limit: self.limit.unwrap_or(100),
            offset: self.offset.unwrap_or(0),
        }
    }
}

struct QueryResult<T> {
    data: T,
    limit: usize,
    offset: usize,
}

impl<T: std::fmt::Display> std::fmt::Display for QueryResult<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Query(data: {}, limit: {}, offset: {})", 
               self.data, self.limit, self.offset)
    }
}

fn main() {
    let result = Query::new("users")
        .limit(10)
        .offset(20)
        .execute();
    
    println!("{}", result);
    
    let numeric_result = Query::new(42)
        .limit(5)
        .execute();
    
    println!("{}", numeric_result);
}

Type Erasure Patterns

trait Draw {
    fn draw(&self);
}

struct Circle {
    radius: f64,
}

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

impl Draw for Circle {
    fn draw(&self) {
        println!("Drawing circle with radius {}", self.radius);
    }
}

impl Draw for Rectangle {
    fn draw(&self) {
        println!("Drawing rectangle {}x{}", self.width, self.height);
    }
}

// Generic container that can hold any drawable
struct Canvas {
    shapes: Vec<Box<dyn Draw>>,
}

impl Canvas {
    fn new() -> Self {
        Canvas {
            shapes: Vec::new(),
        }
    }
    
    fn add_shape<T: Draw + 'static>(mut self, shape: T) -> Self {
        self.shapes.push(Box::new(shape));
        self
    }
    
    fn draw_all(&self) {
        for shape in &self.shapes {
            shape.draw();
        }
    }
}

fn main() {
    let canvas = Canvas::new()
        .add_shape(Circle { radius: 5.0 })
        .add_shape(Rectangle { width: 10.0, height: 3.0 })
        .add_shape(Circle { radius: 2.0 });
    
    canvas.draw_all();
}

Performance Considerations

Monomorphization

Rust creates specialized versions of generic code for each concrete type:

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

fn main() {
    // Compiler generates:
    // - generic_function::<i32>(value: i32)
    // - generic_function::<&str>(value: &str)
    
    generic_function(42);
    generic_function("hello");
}

Zero-Cost Abstractions

Generics have no runtime overhead:

// Generic version
fn add_generic<T>(a: T, b: T) -> T 
where
    T: std::ops::Add<Output = T>,
{
    a + b
}

// Specialized version (what compiler generates)
fn add_i32(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    // These have identical performance
    let result1 = add_generic(5, 3);
    let result2 = add_i32(5, 3);
    
    println!("Generic: {}, Specialized: {}", result1, result2);
}

Best Practices

1. Use Meaningful Type Parameter Names

// Good: Descriptive names
fn process_data<Input, Output, Error>(input: Input) -> Result<Output, Error>
where
    Input: Clone,
    Output: Default,
    Error: std::fmt::Display,
{
    // Implementation
    Ok(Output::default())
}

// Avoid: Single letters when unclear
fn process<T, U, V>(t: T) -> Result<U, V> {
    // What do T, U, V represent?
    todo!()
}

2. Use Where Clauses for Complex Bounds

// Good: Clear with where clause
fn complex_function<T, U, V>(a: T, b: U) -> V
where
    T: Clone + std::fmt::Debug + Send,
    U: std::fmt::Display + Sync,
    V: Default + From<T>,
{
    V::from(a)
}

// Avoid: Inline bounds for complex cases
// fn complex_function<T: Clone + std::fmt::Debug + Send, U: std::fmt::Display + Sync, V: Default + From<T>>(a: T, b: U) -> V {

3. Prefer Associated Types for Unique Relationships

// Good: One logical iterator item type
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

// Use generics for multiple valid implementations
trait From<T> {
    fn from(value: T) -> Self;
}

4. Don't Over-Generalize

// Good: Specific when you don't need generics
fn process_numbers(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

// Unnecessary: Generic when you only use one type
fn process_numbers_generic<T>(numbers: &[T]) -> T 
where
    T: std::iter::Sum<T> + Copy,
{
    numbers.iter().copied().sum()
}

5. Use Phantom Types for Type Safety

use std::marker::PhantomData;

struct Meters(f64);
struct Feet(f64);

struct Distance<Unit> {
    value: f64,
    _unit: PhantomData<Unit>,
}

impl<Unit> Distance<Unit> {
    fn new(value: f64) -> Self {
        Distance {
            value,
            _unit: PhantomData,
        }
    }
    
    fn value(&self) -> f64 {
        self.value
    }
}

impl Distance<Meters> {
    fn to_feet(self) -> Distance<Feet> {
        Distance::new(self.value * 3.28084)
    }
}

fn main() {
    let distance_m = Distance::<Meters>::new(100.0);
    let distance_ft = distance_m.to_feet();
    
    println!("{}m = {}ft", 100.0, distance_ft.value());
}

Generics are a powerful feature that enables writing flexible, reusable code while maintaining Rust's performance and safety guarantees. They allow you to abstract over types and lifetimes, creating APIs that are both expressive and efficient. Master generics to write more maintainable and elegant Rust code.