Phase 4Code organization

#15 Generics

<T>, where, bounds

Generic functions

Generics let you write code that works with many types without duplication. You declare a type parameter in angle brackets after the function name:

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

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    println!("Largest number: {}", largest(&numbers));

    let chars = vec!['a', 'z', 'm', 'b'];
    println!("Largest char: {}", largest(&chars));
}

The compiler generates specialized versions for each type you use -- this is called monomorphization. There is no runtime cost.

Generic structs

Structs can also be generic. This lets you create data structures that work with any type:

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

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

fn main() {
    let int_point = Point::new(5, 10);
    let float_point = Point::new(1.5, 3.7);

    println!("Int: ({}, {})", int_point.x, int_point.y);
    println!("Float: ({}, {})", float_point.x, float_point.y);
}

You can use multiple type parameters to allow different types for each field:

Multiple type parameters
struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let mixed = Point { x: 5, y: 10.5 };
    println!("Point {{ x: {}, y: {} }}", mixed.x, mixed.y);
}

Generic enums

You have already used generic enums: Option<T> and Result<T, E> are both generic! Here is how you define your own:

Generic enum
enum Either<L, R> {
    Left(L),
    Right(R),
}

fn divide(a: f64, b: f64) -> Either<f64, String> {
    if b == 0.0 {
        Either::Right(String::from("Cannot divide by zero"))
    } else {
        Either::Left(a / b)
    }
}

fn main() {
    match divide(10.0, 3.0) {
        Either::Left(result) => println!("Result: {:.2}", result),
        Either::Right(err) => println!("Error: {}", err),
    }
}

Trait bounds with generics

Often you need the generic type to have certain capabilities. You specify this with trait bounds:

Trait bounds
use std::fmt::Display;

// T must implement Display so we can print it
fn print_value<T: Display>(value: T) {
    println!("Value: {}", value);
}

// Multiple bounds with +
fn print_and_clone<T: Display + Clone>(value: T) {
    let cloned = value.clone();
    println!("Original: {}, Clone: {}", value, cloned);
}

fn main() {
    print_value(42);
    print_value("hello");
    print_and_clone(String::from("Rust"));
}

The where clause

When trait bounds get complex, the where clause makes signatures more readable:

The where clause
use std::fmt::{Display, Debug};

// Without where (hard to read)
fn complex<T: Display + Clone, U: Debug + PartialOrd>(t: T, u: U) {
    println!("{}, {:?}", t, u);
}

// With where (much cleaner)
fn complex_clean<T, U>(t: T, u: U)
where
    T: Display + Clone,
    U: Debug + PartialOrd,
{
    println!("{}, {:?}", t, u);
}

You can also use where clauses on impl blocks:

where on impl
struct Wrapper<T> {
    value: T,
}

impl<T> Wrapper<T>
where
    T: Display,
{
    fn show(&self) {
        println!("{}", self.value);
    }
}

impl<T> Wrapper<T>
where
    T: Display + Into<f64> + Copy,
{
    fn distance_from_origin(&self) -> f64 {
        let val: f64 = self.value.into();
        val.abs()
    }
}

fn main() {
    let w = Wrapper { value: 5.0_f64 };
    w.show();
    println!("Distance from origin: {:.2}", w.distance_from_origin());
}

Summary

Quick reference
fn name<T>(x: T)                  // Generic function
struct Name<T> { field: T }       // Generic struct
enum Name<T> { Variant(T) }       // Generic enum

<T: Trait>                        // Trait bound
<T: Trait1 + Trait2>              // Multiple bounds
where T: Trait1 + Trait2          // where clause

impl<T> Name<T> { }               // Generic impl
impl<T: Trait> Name<T> { }        // Conditional impl

Your turn

Try running the generics demo with cargo run and verify with cargo check:

terminal — cargo
user@stemlegacy:~/generics-demo$