Generics


Generics are fundamental for Rust.

Generic Structs

Structs can have type parameters.

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

fn main() {
    let point = Point { x: 1_u32, y: 2 };
    let point: Point<i32> = Point { x: 1, y: 2 };
}

Note:

The part <Precision> introduces a type parameter called Precision. Often people just use T but you don't have to!

Type Inference

  • Inside a function, Rust can look at the types and infer the types of variables and type parameters.
  • Rust will only look at other signatures, never other bodies.
  • If the function signature differs from the body, the body is wrong.

Generic Enums

Enums can have type parameters.

enum Either<T, X> {
    Left(T),
    Right(X),
}

fn main() {
    let alternative: Either<i32, f64> = Either::Left(123);
}

Note:

What happens if I leave out the <i32, f64> specifier? What would type parameter X be set to?

Generic Functions

Functions can have type parameters.

#![allow(unused)]
fn main() {
fn print_stuff<X>(value: X) {
    // What can you do with `value` here?
}
}

Note:

Default bounds are Sized, so finding the size of the type is one thing that you can do. You can also take a reference or a pointer to the value.

Generic Implementations

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

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

impl Vector<f32> {
    fn magnitude(&self) -> f32 {
        ((self.x * self.x) + (self.y * self.y)).sqrt()
    }
}

fn main() {
    let v1 = Vector::new(1.0, 1.0);
    println!("{}", v1.magnitude());
    let v2 = Vector::new(1, 1);
    // println!("{}", v2.magnitude());
}

Note:

Can I call my_vector.magnitude() if T is ... a String? A Person? A TCPStream?

Are there some trait bounds we could place on T such that T + T -> T and T * T -> T and T::sqrt() were all available?

The error:

error[E0599]: no method named `magnitude` found for struct `Vector<{integer}>` in the current scope
  --> src/main.rs:23:23
   |
2  | struct Vector<T> {
   | ---------------- method `magnitude` not found for this struct
 ...
23 |     println!("{}", v2.magnitude());
   |                       ^^^^^^^^^ method not found in `Vector<{integer}>`
   |
   = note: the method was found for
           - `Vector<f32>`
For more information about this error, try `rustc --explain E0599`.

Adding Bounds

  • Generics aren't much use without bounds.
  • A bound says which traits must be implemented on any type used for that type parameter
  • You can apply the bounds on the type, or a function/method, or both.

Adding Bounds - Example

trait HasArea {
    fn area(&self) -> f32;
}

fn print_area<T>(shape: &T) where T: HasArea {
    let area = shape.area();
    println!("Area = {area:?}");
}

struct UnitSquare;

impl HasArea for UnitSquare {
    fn area(&self) -> f32 {
        1.0
    }
}

fn main() {
    let u = UnitSquare;
    print_area(&u);
}

Adding Bounds - Alt. Example

trait HasArea {
    fn area(&self) -> f32;
}

fn print_area<T: HasArea>(shape: &T) {
    let area = shape.area();
    println!("Area = {area:?}");
}

struct UnitSquare;

impl HasArea for UnitSquare {
    fn area(&self) -> f32 {
        1.0
    }
}

fn main() {
    let u = UnitSquare;
    print_area(&u);
}

Note:

This is exactly equivalent to the previous example, but shorter. However, if you end up with a large set of bounds, they are easier to format when at the end of the line.

General Rule

  • If you can, try and avoid adding bounds to structs.
  • Simpler to only add them to the methods.

Multiple Bounds

You can specify multiple bounds.

trait HasArea {
    fn area(&self) -> f32;
}

fn print_area<T: std::fmt::Debug + HasArea>(shape: &T) {
    println!("Shape {:?} has area {}", shape, shape.area());
}

#[derive(Debug)]
struct UnitSquare;

impl HasArea for UnitSquare {
    fn area(&self) -> f32 { 1.0 }
}

fn main() {
    let u = UnitSquare;
    print_area(&u);
}

impl Trait

  • The impl Trait syntax in argument position was just syntactic sugar.
  • (It does something special in the return position though)
#![allow(unused)]
fn main() {
trait HasArea {
    fn area_m2(&self) -> f64;
}

struct AreaCalculator {
    area_m2: f64
}

impl AreaCalculator {
    // Same: fn add(&mut self, shape: impl HasArea) {
    fn add<T: HasArea>(&mut self, shape: T) {
        self.area_m2 += shape.area_m2();
    }
}
}

Note:

Some types that cannot be written out, like the closure, can be expressed as return types using impl. e.g. fn score(y: i32) -> impl Fn(i32) -> i32.

Caution

  • Using Generics is Hard Mode Rust
  • Don't reach for it in the first instance...
    • Try and just use concrete types?

Generic over Constants

In Rust 1.51, we gained the ability to be generic over constant values too.

struct Polygon<const SIDES: u8> {
    colour: u32
}

impl<const SIDES: u8> Polygon<SIDES> {
    fn new(colour: u32) -> Polygon<SIDES> { Polygon { colour } }
    fn print(&self) { println!("{} sides, colour=0x{:06x}", SIDES, self.colour); }
}

fn main() {
    let triangle: Polygon<3> = Polygon::new(0x00FF00);
    triangle.print();
}

Note:

SIDES is a property of the type, and doesn't occupy any memory within any values of that type at run-time - the constant is pasted in wherever it is used.

Generic Traits

Traits themselves can have type parameters too!

trait HasArea<T> {
    fn area(&self) -> T;
}
 
// Here we only accept a shape where the `U` in `HasArea<Y>` is printable
fn print_area<T, U>(shape: &T) where T: HasArea<U>, U: std::fmt::Debug {
    let area = shape.area();
    println!("Area = {area:?}");
}

struct UnitSquare;

impl HasArea<f64> for UnitSquare {
    fn area(&self) -> f64 {
        1.0
    }
}
fn main() {
    let u = UnitSquare;
    print_area(&u);
}

Special Bounds

  • Some bounds apply automatically
  • Special syntax to turn them off
#![allow(unused)]
fn main() {
fn print_debug<T: std::fmt::Debug + ?Sized>(value: &T) {
    println!("value is {:?}", value);
}
}

Note:

This bound says "It must implement std::fmt::Debug, but I don't care if it has a size known at compile-time".

Things that don't have sizes known at compile time (but which may or may not implement std::fmt::Debug) include:

  • String Slices
  • Closures