Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New (much smaller) 2.4.1 exercise #95

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion book/src/advanced-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Follow the instructions in the comments of `exercises/2-foundations-of-rust/3-ad

Follow the instructions in the comments of `exercises/2-foundations-of-rust/3-advanced-syntax/3-slices/src/main.rs`!
Don't take too much time on the extra assignment, instead come back later once
you've done the rest of the excercises.
you've done the rest of the exercises.
## Exercise 2.3.4: Ring Buffer

This is a bonus exercise! Follow the instructions in the comments of
Expand Down
62 changes: 50 additions & 12 deletions book/src/traits-and-generics.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,37 @@

<a href="/slides/2_4-traits-and-generics/" target="_blank">Slides</a>

## Exercise 2.4.1: Local Storage Vec
## Exercise 2.4.1: Vector math

In this exercise, we'll create a type called `LocalStorageVec`, which is generic list of items that resides either on the stack or the heap, depending on its size. If its size is small enough for items to be put on the stack, the `LocalStorageVec` buffer is backed by an array. `LocalStorageVec` is not only generic over the type (`T`) of items in the list, but also by the size (`N`) of this stack-located array using a relatively new feature called ["const generics"](https://doc.rust-lang.org/reference/items/generics.html#const-generics). Once the `LocalStorageVec` contains more items than fit in the array, a heap based [`Vec`](https://doc.rust-lang.org/std/vec/struct.Vec.html) is allocated as space for the items to reside in.
In this exercise, we'll implement some basic vector math operations for our 2-dimensional vector type `Vec2D`.

### 2.4.1.A Vector addition
Let's start by implementing vector addition for `Vec2D` as Rust's `std::ops::Add` trait. Replace the `todo!()` such that it returns a new `Vec2D` of which the `x` component is the sum of the `x` components of the two input vectors, and similarly for the `y` component. If implemented correctly, the `integer_addition` test should now pass!

### 2.4.1.B Dot product
Now, let's implement the [dot product](https://en.wikipedia.org/wiki/Dot_product) for our vector type. Multiplying two vectors with a dot product should return a singular value, which is the sum of the products of the components. For example, `[1, 2] * [3, 4] = 1 * 3 + 2 * 4 = 11`. Implement this by adding a new `impl` block that implements the `std::ops::Mul` trait for `Vec2D`. Uncomment the `integer_dot_product` test to test your code!

### 2.4.1.C Making `Vec2D` generic
Currently, the `x` and `y` components of our `Vec2D` can only be 32-bit integers. What if we want to use other number types, such as floating-point numbers? Let's make `Vec2D` generic over a type `T` by changing the definition of `Vec2D` such that the `x` and `y` are generic.

We will also need to update our addition and dot product implementations to support this generic type `T`. Instead of implementing the `add` and `mul` functions for every type of number separately, we can leave the implementations generic by implementing them for any `T` that can be added/multiplied (i.e. any type `T` that has the `Add` and/or `Mul` trait). This means we can just change the start of the `impl` block, without needing to change the actual function implementations!

<details>
<summary><b>Hint 1 (generic implementations with trait bounds)</b></summary>
You can add a generic type to an `impl` block by writing `impl<T>`. This generic type can be bound to a trait by writing e.g. `impl<T: Display>` to specify that type `T` must have the `Display` trait (which you can also write as `impl<T> ... where T: Display`). You can add multiple trait bounds to a type by writing e.g. `impl<T: Clone + Display>`.
</details>

<details>
<summary><b>Hint 2 (the `Add` and `Mul` traits)</b></summary>
The `Add` and `Mul` traits have a generic `Output` type. This allowed us to implement the dot product as the `Mul` trait of `Vec2D` by setting the output to be a number. For our generic implementation, we want the generic type `T` to be something we can add and/or multiply. To specify this, we add `Add` and/or `Mul` trait bounds to `T`, but we also need to specify what output we expect from adding or multiplying two `T` values by writing e.g. `T: Add<Output = T>`.
</details>

Uncomment the `float_addition` and `float_dot_product` tests to check if our `Vec2D` now works with floating-point values!


## Exercise 2.4.2: Local Storage Vec

This is a bonus exercise! We'll create a type called `LocalStorageVec`, which is generic list of items that resides either on the stack or the heap, depending on its size. If its size is small enough for items to be put on the stack, the `LocalStorageVec` buffer is backed by an array. `LocalStorageVec` is not only generic over the type (`T`) of items in the list, but also by the size (`N`) of this stack-located array using a relatively new feature called ["const generics"](https://doc.rust-lang.org/reference/items/generics.html#const-generics). Once the `LocalStorageVec` contains more items than fit in the array, a heap based [`Vec`](https://doc.rust-lang.org/std/vec/struct.Vec.html) is allocated as space for the items to reside in.

**Within this exercise, the objectives are annotated with a number of stars (⭐), indicating the difficulty. You are likely not to be able to finish all exercises during the tutorial session**

Expand All @@ -14,7 +42,7 @@ In this exercise, we'll create a type called `LocalStorageVec`, which is generic

Open the `exercises/2-foundations-of-rust/4-traits-and-generics/1-local-storage-vec` crate. It contains a `src/lib.rs` file, meaning this crate is a library. `lib.rs` contains a number of tests, which can be run by calling `cargo test`. Don't worry if they don't pass or even compile right now: it's your job to fix that in this exercise. Most of the tests are commented out right now, to enable a step-by-step approach. **Before you begin, have a look at the code and the comments in there, they contain various helpful clues.**

### 2.4.1.A Defining the type ⭐
### 2.4.2.A Defining the type ⭐
Currently, the `LocalStorageVec` `enum` is incomplete. Give it two variants: `Stack` and `Heap`. `Stack` contains two named fields, `buf` and `len`. `buf` will be the array with a capacity to hold `N` items of type `T`; `len` is a field of type `usize` that will denote the amount of items actually stored. The `Heap` variant has an unnamed field containing a `Vec<T>`. If you've defined the `LocalStorageVec` variants correctly, running `cargo test` should output something like

```txt
Expand Down Expand Up @@ -50,7 +78,7 @@ pub enum LocalStorageVec<T, const N: usize> {
```
</details>

### 2.4.1.B `impl`-ing `From<Vec<T>>` ⭐
### 2.4.2.B `impl`-ing `From<Vec<T>>` ⭐

Uncomment the test `it_from_vecs`, and add an implementation for `From<Vec<T>>` to `LocalStorageVec<T>`. To do so, copy the following code in your `lib.rs` file and replace the `todo!` macro invocation with your code that creates a heap-based `LocalStorageVec` containing the passed `Vec<T>`.

Expand All @@ -67,7 +95,17 @@ impl<T, const N: usize> From<Vec<T>> for LocalStorageVec<T, N> {

Run `cargo test` to validate your implementation.

### 2.4.1.C `impl LocalStorageVec` ⭐⭐
### 2.4.2.C `AsRef` and `AsMut` ⭐⭐
[`AsRef`](https://doc.rust-lang.org/std/convert/trait.AsRef.html) and [`AsMut`](https://doc.rust-lang.org/std/convert/trait.AsMut.html) are used to implement *cheap* reference-to-reference coercion. For instance, our `LocalStorageVec<T, N>` is somewhat similar to a slice `&[T]`, as both represent a contiguous series of `T` values. This is true whether the `LocalStorageVec` buffer resides on the stack or on the heap.

Uncomment the `it_as_refs` test case and implement `AsRef<[T]>` and `AsMut<[T]>`.

<details>
<summary><b>Hint</b></summary>
Make sure to take into account the value of `len` for the `Stack` variant of `LocalStorageVec` when creating a slice.
</details>

### 2.4.2.D `impl LocalStorageVec` ⭐⭐
To make the `LocalStorageVec` more useful, we'll add more methods to it.
Create an `impl`-block for `LocalStorageVec`.
Don't forget to declare and provide the generic parameters.
Expand All @@ -86,7 +124,7 @@ The next methods we'll implement are `len`, `push`, `pop`, `insert`, `remove` an
Uncomment the corresponding test cases and make them compile and pass. **Be sure to have a look at the methods provided for slices [`[T]`](https://doc.rust-lang.org/std/primitive.slice.html) and [`Vec<T>`](https://doc.rust-lang.org/std/vec/struct.Vec.html)** Specifically, `[T]::copy_within` and `Vec::extend_from_slice` can be of use.


### 2.4.1.E `Iterator` and `IntoIterator` ⭐⭐
### 2.4.2.E `Iterator` and `IntoIterator` ⭐⭐
Our `LocalStorageVec` can be used in the real world now, but we still shouldn't be satisfied. There are various traits in the standard library that we can implement for our `LocalStorageVec` that would make users of our crate happy.

First off, we will implement the [`IntoIterator`](https://doc.rust-lang.org/std/iter/trait.IntoIterator.html) and [`Iterator`](https://doc.rust-lang.org/std/iter/trait.Iterator.html) traits. Go ahead and uncomment the `it_iters` test case. Let's define a new type:
Expand All @@ -105,7 +143,7 @@ Take a look at the list of methods under the ['provided methods' section](https:

Now to instantiate a `LocalStorageVecIter`, implement the [`IntoIter`] trait for it, in such a way that calling `into_iter` yields a `LocalStorageVecIter`.

### 2.4.1.F `Index` ⭐⭐
### 2.4.2.F `Index` ⭐⭐
To allow users of the `LocalStorageVec` to read items or slices from its buffer, we can implement the [`Index`](https://doc.rust-lang.org/std/ops/trait.Index.html) trait. This trait is generic over the type of the item used for indexing. In order to make our `LocalStorageVec` versatile, we should implement:

- `Index<usize>`, allowing us to get a single item by calling `vec[1]`;
Expand All @@ -115,11 +153,11 @@ To allow users of the `LocalStorageVec` to read items or slices from its buffer,

Each of these implementations can be implemented in terms of the `as_ref` implementation, as slices `[T]` all support indexing by the previous types. That is, `[T]` also implements `Index` for those types. Uncomment the `it_indexes` test case and run `cargo test` in order to validate your implementation.

### 2.4.1.G Removing bounds ⭐⭐
### 2.4.2.G Removing bounds ⭐⭐
When we implemented the borrowing `Iterator`, we saw that it's possible to define methods in separate `impl` blocks with different type bounds. Some of the functionality you wrote used the assumption that `T` is both `Copy` and `Default`. However, this means that each of those methods are only defined for `LocalStorageVec`s containing items of type `T` that in fact do implement `Copy` and `Default`, which is not ideal. How many methods can you rewrite having one or both of these bounds removed?


### 2.4.1.H Borrowing `Iterator` ⭐⭐⭐
### 2.4.2.H Borrowing `Iterator` ⭐⭐⭐
We've already got an iterator for `LocalStorageVec`, though it has the limitation that in order to construct it, the `LocalStorageVec` needs to be consumed. What if we only want to iterate over the items, and not consume them? We will need another iterator type, one that contains an immutable reference to the `LocalStorageVec` and that will thus need a lifetime annotation. Add a method called `iter` to `LocalStorageVec` that takes a shared `&self` reference, and instantiates the borrowing iterator. Implement the `Iterator` trait with the appropriate `Item` reference type for your borrowing iterator. To validate your code, uncomment and run the `it_borrowing_iters` test case.

Note that this time, the test won't compile if you require the items of `LocalStorageVec` be `Copy`! That means you'll have to define `LocalStorageVec::iter` in a new `impl` block that does not put this bound on `T`:
Expand All @@ -136,8 +174,8 @@ impl<T: const N: usize> LocalStorageVec<T, N> {

Defining methods in separate `impl` blocks means some methods are not available for certain instances of the generic type. In our case, the `new` method is only available for `LocalStorageVec`s containing items of type `T` that implement both `Copy` and `Default`, but `iter` is available for all `LocalStorageVec`s.

### 2.4.1.I Generic `Index` ⭐⭐⭐⭐
You've probably duplicated a lot of code in exercise 2.4.1.F. We can reduce the boilerplate by defining an empty trait:
### 2.4.2.I Generic `Index` ⭐⭐⭐⭐
You've probably duplicated a lot of code in exercise 2.4.2.F. We can reduce the boilerplate by defining an empty trait:

```rust
trait LocalStorageVecIndex {}
Expand All @@ -154,7 +192,7 @@ Next, replace the multiple implementations of `Index` with a single implementati

If you've done this correctly, `it_indexes` should again compile and pass.

### 2.4.1.J `Deref` and `DerefMut` ⭐⭐⭐⭐
### 2.4.2.J `Deref` and `DerefMut` ⭐⭐⭐⭐
The next trait that makes our `LocalStorageVec` more flexible in use are [`Deref`](https://doc.rust-lang.org/std/ops/trait.Deref.html) and [`DerefMut`](https://doc.rust-lang.org/std/ops/trait.DerefMut.html) that utilize the 'deref coercion' feature of Rust to allow types to be treated as if they were some type they look like.
That would allow us to use any [method that is defined on `[T]`](https://doc.rust-lang.org/std/primitive.slice.html) by calling them on a `LocalStorageVec`.
Before continuing, read the section ['Treating a Type Like a Reference by Implementing the Deref Trait'](https://doc.rust-lang.org/book/ch15-02-deref.html#treating-a-type-like-a-reference-by-implementing-the-deref-trait) from The Rust Programming Language (TRPL).
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "vector-math"
version = "0.1.0"
edition = "2021"

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::ops::{Add, Mul};

#[derive(Copy, Clone)]
struct Vec2D {
x: i32,
y: i32,
}

impl Add for Vec2D {
type Output = Self;

fn add(self, rhs: Self) -> Self::Output {
todo!();
}
}

#[cfg(test)]
mod test {
use crate::Vec2D;

#[test]
fn integer_addition() {
let a = Vec2D { x: 1, y: 2 };
let b = Vec2D { x: 3, y: 4 };
let res = a + b;
assert_eq!(res.x, a.x + b.x);
assert_eq!(res.y, a.y + b.y);
}

// #[test]
// fn integer_dot_product() {
// let a = Vec2D { x: 1, y: 2 };
// let b = Vec2D { x: 3, y: 4 };
// let res = a * b;
// assert_eq!(res, a.x * b.x + a.y * b.y);
// }

// #[test]
// fn float_addition() {
// let a = Vec2D { x: 1.5, y: 2.5 };
// let b = Vec2D { x: 3.7, y: 4.2 };
// let res = a + b;
// assert_eq!(res.x, a.x + b.x);
// assert_eq!(res.y, a.y + b.y);
// }

// #[test]
// fn float_dot_product() {
// let a = Vec2D { x: 1.5, y: 2.5 };
// let b = Vec2D { x: 3.7, y: 4.2 };
// let res = a * b;
// assert_eq!(res, a.x * b.x + a.y * b.y);
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ mod test {
fn it_compiles() {
// Here's a trick to 'initialize' a type while not actually
// creating a value: an infinite `loop` expression diverges
// and evaluates to the 'never type' `!`, which, as is can never
// and evaluates to the 'never type' `!`, which, as it can never
// actually be instantiated, coerces to any other type.
// Some other ways of diverging are by calling the `panic!` or the `todo!`
// macros.
Expand Down Expand Up @@ -191,7 +191,6 @@ mod test {
// fn it_removes() {
// let mut vec: LocalStorageVec<_, 4> = LocalStorageVec::from([0, 1, 2]);
// let elem = vec.remove(1);
// dbg!(&vec);
// assert!(matches!(
// vec,
// LocalStorageVec::Stack {
Expand Down