From 45222fb2ecb4b88429286e037686f57162480804 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Mon, 12 Jun 2023 18:01:42 +0100 Subject: [PATCH] Add notes on generics. --- training-slides/src/generics.md | 291 +++++++++++++++++++++++++++++++- 1 file changed, 290 insertions(+), 1 deletion(-) diff --git a/training-slides/src/generics.md b/training-slides/src/generics.md index ca7b515..6ea4614 100644 --- a/training-slides/src/generics.md +++ b/training-slides/src/generics.md @@ -1 +1,290 @@ -# Generic over Types +# Generics + +--- + +Generics are fundamental for Rust. + +## Generic Structs + +Structs can have type parameters. + +```rust [] +struct Point { + x: Precision, + y: Precision, +} + +fn main() { + let point = Point { x: 1_u32, y: 2 }; + let point: Point = Point { x: 1, y: 2 }; +} +``` + +Note: + +The part `` 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. + +```rust [] +enum Either { + Left(T), + Right(X), +} + +fn main() { + let alternative: Either = Either::Left(123); +} +``` + +Note: + +What happens if I leave out the `` specifier? What would type parameter `X` be set to? + +## Generic Functions + +Functions can have type parameters. + +```rust [] +fn print_stuff(value: X) { + // What can you do with `value` here? +} +``` + +## Generic Implementations + +```rust [] +struct Vector { + x: T, + y: T, +} + +impl Vector { + fn new(x: T, y: T) -> Vector { + Vector { x, y } + } +} + +impl Vector { + 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: + +```text +error[E0599]: no method named `magnitude` found for struct `Vector<{integer}>` in the current scope + --> src/main.rs:22:23 + | +1 | struct Vector { + | ---------------- method `magnitude` not found for this struct +... +22 | println!("{}", v2.magnitude()); + | ^^^^^^^^^ method not found in `Vector<{integer}>` + | + = note - the method was found for + - `Vector` + +For more information about this error, try `rustc --explain E0599`. +``` + +## Generic Traits + +Traits can have type parameters. + +```rust [] +trait HasArea { + fn area(&self) -> T; +} + +// Here we only accept a shape where the `T` in `HasArea` is `f64` +fn print_area(shape: &dyn HasArea) { + let area = shape.area(); + println!("Area = {area:0.6}"); +} + +struct UnitSquare; + +impl HasArea for UnitSquare { + fn area(&self) -> f64 { + 1.0 + } +} + +impl HasArea for UnitSquare { + fn area(&self) -> u32 { + 1 + } +} + +fn main() { + let u = UnitSquare; + print_area(&u); +} +``` + +## Adding Bounds + +* Generics aren't much use without bounds. +* You can apply the bounds on the type, or a function, or both. + +```rust [] +trait HasArea { + fn area(&self) -> T; +} + +fn print_area(shape: &dyn HasArea) where T: std::fmt::Debug { + let area = shape.area(); + println!("Area = {area:?}"); +} + +struct UnitSquare; + +impl HasArea for UnitSquare { + fn area(&self) -> f64 { + 1.0 + } +} + +fn main() { + let u = UnitSquare; + print_area(&u); +} +``` + +## Adding Bounds + +The bounds can also go here: + +```rust [] +trait HasArea { + fn area(&self) -> T; +} + +fn print_area(shape: &dyn HasArea) { + let area = shape.area(); + println!("Area = {area:?}"); +} +``` + +Note: + +This is exactly equivalent to the previous example, but shorter. + +## 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. + +```rust [] +trait HasArea { + fn area(&self) -> T; +} + +fn print_areas( + shape1: &dyn HasArea, + shape2: &dyn HasArea, +) { + let area1 = shape1.area(); + let area2 = shape2.area(); + if area1 == area2 { + println!("Both areas are {area1:?}"); + } else { + println!("{area1:?}, {area2:?}"); + } +} + +struct UnitSquare; + +impl HasArea for UnitSquare { + fn area(&self) -> f64 { + 1.0 + } +} + +fn main() { + let u1 = UnitSquare; + let u2 = UnitSquare; + print_areas(&u1, &u2); +} +``` + +Note: + +Try removing the `std::cmp::PartialEq` bound and see what it says about using the `==` operator on type `T`. + +## impl Trait + +* The `impl Trait` syntax in argument position was just *syntactic sugar*. +* (It does something special in the return position though) + +```rust [] +trait HasArea { + fn area_m2(&self) -> f64; +} + +struct AreaCalculator { + area_m2: f64 +} + +impl AreaCalculator { + // Same: fn add(&mut self, shape: impl HasArea) { + fn add(&mut self, shape: T) { + self.area_m2 += shape.area_m2(); + } +} +``` + +## Caution + +* Using Generics is *Hard Mode Rust* +* Don't reach for it in the first instance... + * Try and just use concrete types? + +## Special Bounds + +* Some bounds apply automatically +* Special syntax to *turn them off* + +```rust [] +fn print_debug(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 +