CS3984 Computer Systems in Rust



Generics

  1. Generics allow generalizing functions and types to reduce code duplication.

  2. Generics are used in type parameters.

  3. Type parameters are declared inside angle brackets and are conventionally uppercase letters (eg. <T>).

Using Generics

fn main() {
    let v1: Vec<i32> = vec![1, 2, 3, 4, 5];
    let v2: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    let v3: Vec<&str> = vec!["crab", "penguin", "language"];
}


The type of a vector is Vec<T>.

fn main() {
    let maybe_int: Option<i32> = Some(5);
    let maybe_int_2: Option<i32> = None;
}


The type of an option is Option<T>.

Generics in Structs

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

fn main() {
    let integer: Point<i32> = Point { x: 5, y: 10 };
    let float: Point<f32> = Point { x: 1.0, y: 4.0 };
}


  • The type Point<T> is generic over the type T
  • The fields x and y can be any type T, but they must be the same type.

Generics in Enums

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

fn main() {
    let int_or_float: Either<i32, f32> = Either::Left(5);
    let int_or_float_2: Either<i32, f32> = Either::Right(100.0);

    let int_or_int: Either<i32, i32> = Either::Left(100);
}


  • The type Either<L, R> is generic over the types L and R
  • L and R can be the same type, but they don’t have to be!

Generics in Functions

Without generics:

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

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


Generics in Functions (2)

With generics:

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


  • The function largest is generic over type T, which normally can be any type…
  • BUT since we want to compare item > largest, we need to restrict the possible types T can be to types that can be compared (implements the PartialOrd trait)

Generics in Methods

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

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }

    fn y(&self) -> &T {
        &self.y
    }
}


The <T> after impl allows us to refer to the T in Point<T> inside the impl block.

Generics in Methods (2)

Concrete methods can be implemented for generic types by specifying a concrete type for generics:

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

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


Performance of Generics

Rust performs monomorphization of generic code, meaning the compiler turns generic code into specific code by generating separate code for each concrete type:

let integer = Some(5);
let float = Some(5.0);


is automatically turned at compile time into:

enum Option_i32 {
    Some(i32),
    None,
}
enum Option_f64 {
    Some(f64),
    None,
}
fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}


Traits

  • A type is a set of values and a set of operations we can perform on those values
  • The set of operations are the methods we can call on that type
  • Different types share the same behavior if we can call the same methods on all of those types
  • A trait is a grouping of methods to define a set of behaviors necessary to accomplish some purpose
trait Summary {
    fn summarize(&self) -> String;
}


The Summary trait requires implementors to define a method named summarize that takes &self and returns a String.

What the implementor does under the hood is no concern to the trait as long as the method signature is upheld.

Trait Implementation

A struct can implement a trait by using the impl Trait for Type syntax:

pub struct NewsArticle {
    pub headline: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {}", self.headline, self.author)
    }
}


Trait Implementation (2)

The same trait can be implemented by multipe types:

pub struct Tweet {
    pub username: String,
    pub content: String,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}


Default Trait Implementation

Traits can have default implementations:

trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {}


The corresponding implementation block for a type can then be left empty to use the default implementation of the trait.

Default Trait Implementation (2)

Default trait implementations can call other trait methods:

trait Summary {
    fn summarize_author(&self) -> String;
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize_author(&self) -> String {
        format!("Author {}", self.author)
    }
}


Trait bounds in functions

Functions can accept “anything that implements a particular trait”. The following are all equivalent:

  • With impl Trait syntax:

    fn print_summary(item: &impl Summary) {
        println!("Summary: {}", item.summarize());
    }
    
    
    
  • With generics and trait bounds:

    fn print_summary<T: Summary>(item: &T) {
        println!("Summary: {}", item.summarize());
    }
    
    
    

Multiple Trait Bounds

Multiple trait bounds can be specified with the + syntax:

fn print_summary<T: Summary + Display>(item: &T) {
    println!("Summary: {}", item.summarize());
}


The type T must implement both the Summary and Display traits. For functions with many generics and trait bounds, using where clauses can make functions signature more readable:

fn print_summary<T>(item: &T) where T: Summary {
    println!("Summary: {}", item.summarize());
}

fn some_function_2<T, U>(t: &T, u: &U) -> i32
where
  T: Display + Clone,
  U: Clone + Debug
{
  // Function body
}


Conditional Methods

Trait bounds can be used to conditionally implement methods for types that implement the specified traits.

use std::fmt::Display;

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

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}