Skip to content

Commit

Permalink
Merge pull request #63 from ferrous-systems/add-generics
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanpallant committed Jun 13, 2023
2 parents dc5575f + 45222fb commit 51ce803
Showing 1 changed file with 290 additions and 1 deletion.
291 changes: 290 additions & 1 deletion training-slides/src/generics.md
Original file line number Diff line number Diff line change
@@ -1 +1,290 @@
# Generic over Types
# Generics

---

Generics are fundamental for Rust.

## Generic Structs

Structs can have type parameters.

```rust []
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.

```rust []
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.

```rust []
fn print_stuff<X>(value: X) {
// What can you do with `value` here?
}
```

## Generic Implementations

```rust []
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:

```text
error[E0599]: no method named `magnitude` found for struct `Vector<{integer}>` in the current scope
--> src/main.rs:22:23
|
1 | struct Vector<T> {
| ---------------- method `magnitude` not found for this struct
...
22 | 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`.
```

## Generic Traits

Traits can have type parameters.

```rust []
trait HasArea<T> {
fn area(&self) -> T;
}

// Here we only accept a shape where the `T` in `HasArea<T>` is `f64`
fn print_area(shape: &dyn HasArea<f64>) {
let area = shape.area();
println!("Area = {area:0.6}");
}

struct UnitSquare;

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

impl HasArea<u32> 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<T> {
fn area(&self) -> T;
}

fn print_area<T>(shape: &dyn HasArea<T>) where T: 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);
}
```

## Adding Bounds

The bounds can also go here:

```rust []
trait HasArea<T> {
fn area(&self) -> T;
}

fn print_area<T: std::fmt::Debug>(shape: &dyn HasArea<T>) {
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<T> {
fn area(&self) -> T;
}

fn print_areas<T: std::fmt::Debug + std::cmp::PartialEq>(
shape1: &dyn HasArea<T>,
shape2: &dyn HasArea<T>,
) {
let area1 = shape1.area();
let area2 = shape2.area();
if area1 == area2 {
println!("Both areas are {area1:?}");
} else {
println!("{area1:?}, {area2:?}");
}
}

struct UnitSquare;

impl HasArea<f64> 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<T: HasArea>(&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<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

0 comments on commit 51ce803

Please sign in to comment.