Formatter (#51)

Enforce consistent formatting use `dprint`
This commit is contained in:
Luca Palmieri
2024-05-24 17:00:03 +02:00
committed by GitHub
parent 537118574b
commit 99591a715e
157 changed files with 1057 additions and 1044 deletions

View File

@@ -15,4 +15,4 @@ The task will give us an opportunity to explore new Rust concepts, such as:
- `HashMap` and `BTreeMap`, two key-value data structures
- `Eq` and `Hash`, to compare keys in a `HashMap`
- `Ord` and `PartialOrd`, to work with a `BTreeMap`
- `Index` and `IndexMut`, to access elements in a collection
- `Index` and `IndexMut`, to access elements in a collection

View File

@@ -1,14 +1,14 @@
# Arrays
As soon as we start talking about "ticket management" we need to think about a way to store _multiple_ tickets.
In turn, this means we need to think about collections. In particular, homogeneous collections:
In turn, this means we need to think about collections. In particular, homogeneous collections:
we want to store multiple instances of the same type.
What does Rust have to offer in this regard?
## Arrays
A first attempt could be to use an **array**.
A first attempt could be to use an **array**.\
Arrays in Rust are fixed-size collections of elements of the same type.
Here's how you can define an array:
@@ -18,7 +18,7 @@ Here's how you can define an array:
let numbers: [u32; 3] = [1, 2, 3];
```
This creates an array of 3 integers, initialized with the values `1`, `2`, and `3`.
This creates an array of 3 integers, initialized with the values `1`, `2`, and `3`.\
The type of the array is `[u32; 3]`, which reads as "an array of `u32`s with a length of 3".
### Accessing elements
@@ -31,8 +31,8 @@ let second = numbers[1];
let third = numbers[2];
```
The index must be of type `usize`.
Arrays are **zero-indexed**, like everything in Rust. You've seen this before with string slices and field indexing in
The index must be of type `usize`.\
Arrays are **zero-indexed**, like everything in Rust. You've seen this before with string slices and field indexing in
tuples/tuple-like variants.
### Out-of-bounds access
@@ -44,8 +44,8 @@ let numbers: [u32; 3] = [1, 2, 3];
let fourth = numbers[3]; // This will panic
```
This is enforced at runtime using **bounds checking**. It comes with a small performance overhead, but it's how
Rust prevents buffer overflows.
This is enforced at runtime using **bounds checking**. It comes with a small performance overhead, but it's how
Rust prevents buffer overflows.\
In some scenarios the Rust compiler can optimize away bounds checks, especially if iterators are involved—we'll speak
more about this later on.
@@ -77,5 +77,5 @@ Stack: | 1 | 2 | 3 |
```
In other words, the size of an array is `std::mem::size_of::<T>() * N`, where `T` is the type of the elements and `N` is
the number of elements.
You can access and replace each element in `O(1)` time.
the number of elements.\
You can access and replace each element in `O(1)` time.

View File

@@ -18,11 +18,11 @@ error[E0435]: attempt to use a non-constant value in a constant
```
Arrays wouldn't work for our ticket management system—we don't know how many tickets we'll need to store at compile-time.
This is where `Vec` comes in.
This is where `Vec` comes in.
## `Vec`
`Vec` is a growable array type, provided by the standard library.
`Vec` is a growable array type, provided by the standard library.\
You can create an empty array using the `Vec::new` function:
```rust
@@ -37,7 +37,7 @@ numbers.push(2);
numbers.push(3);
```
New values are added to the end of the vector.
New values are added to the end of the vector.\
You can also create an initialized vector using the `vec!` macro, if you know the values at creation time:
```rust
@@ -55,7 +55,7 @@ let second = numbers[1];
let third = numbers[2];
```
The index must be of type `usize`.
The index must be of type `usize`.\
You can also use the `get` method, which returns an `Option<&T>`:
```rust
@@ -70,7 +70,7 @@ Access is bounds-checked, just element access with arrays. It has O(1) complexit
## Memory layout
`Vec` is a heap-allocated data structure.
`Vec` is a heap-allocated data structure.\
When you create a `Vec`, it allocates memory on the heap to store the elements.
If you run the following code:
@@ -102,11 +102,11 @@ Heap: | 1 | 2 | ? |
- The **length** of the vector, i.e. how many elements are in the vector.
- The **capacity** of the vector, i.e. the number of elements that can fit in the space reserved on the heap.
This layout should look familiar: it's exactly the same as `String`!
This layout should look familiar: it's exactly the same as `String`!\
That's not a coincidence: `String` is defined as a vector of bytes, `Vec<u8>`, under the hood:
```rust
pub struct String {
vec: Vec<u8>,
}
```
```

View File

@@ -11,15 +11,15 @@ numbers.push(3); // Max capacity reached
numbers.push(4); // What happens here?
```
The `Vec` will **resize** itself.
The `Vec` will **resize** itself.\
It will ask the allocator for a new (larger) chunk of heap memory, copy the elements over, and deallocate the old memory.
This operation can be expensive, as it involves a new memory allocation and copying all existing elements.
This operation can be expensive, as it involves a new memory allocation and copying all existing elements.
## `Vec::with_capacity`
If you have a rough idea of how many elements you'll store in a `Vec`, you can use the `Vec::with_capacity`
method to pre-allocate enough memory upfront.
If you have a rough idea of how many elements you'll store in a `Vec`, you can use the `Vec::with_capacity`
method to pre-allocate enough memory upfront.\
This can avoid a new allocation when the `Vec` grows, but it may waste memory if you overestimate actual usage.
Evaluate on a case-by-case basis.

View File

@@ -1,6 +1,6 @@
# Iteration
During the very first exercises, you learned that Rust lets you iterate over collections using `for` loops.
During the very first exercises, you learned that Rust lets you iterate over collections using `for` loops.
We were looking at ranges at that point (e.g. `0..5`), but the same holds true for collections like arrays and vectors.
```rust
@@ -35,13 +35,13 @@ loop {
}
```
`loop` is another looping construct, on top of `for` and `while`.
`loop` is another looping construct, on top of `for` and `while`.\
A `loop` block will run forever, unless you explicitly `break` out of it.
## `Iterator` trait
The `next` method in the previous code snippet comes from the `Iterator` trait.
The `Iterator` trait is defined in Rust's standard library and provides a shared interface for
The `Iterator` trait is defined in Rust's standard library and provides a shared interface for
types that can produce a sequence of values:
```rust
@@ -53,16 +53,16 @@ trait Iterator {
The `Item` associated type specifies the type of the values produced by the iterator.
`next` returns the next value in the sequence.
It returns `Some(value)` if there's a value to return, and `None` when there isn't.
`next` returns the next value in the sequence.\
It returns `Some(value)` if there's a value to return, and `None` when there isn't.
Be careful: there is no guarantee that an iterator is exhausted when it returns `None`. That's only
guaranteed if the iterator implements the (more restrictive)
guaranteed if the iterator implements the (more restrictive)
[`FusedIterator`](https://doc.rust-lang.org/std/iter/trait.FusedIterator.html) trait.
## `IntoIterator` trait
Not all types implement `Iterator`, but many can be converted into a type that does.
Not all types implement `Iterator`, but many can be converted into a type that does.\
That's where the `IntoIterator` trait comes in:
```rust
@@ -73,15 +73,15 @@ trait IntoIterator {
}
```
The `into_iter` method consumes the original value and returns an iterator over its elements.
The `into_iter` method consumes the original value and returns an iterator over its elements.\
A type can only have one implementation of `IntoIterator`: there can be no ambiguity as to what `for` should desugar to.
One detail: every type that implements `Iterator` automatically implements `IntoIterator` as well.
One detail: every type that implements `Iterator` automatically implements `IntoIterator` as well.
They just return themselves from `into_iter`!
## Bounds checks
Iterating over iterators has a nice side effect: you can't go out of bounds, by design.
Iterating over iterators has a nice side effect: you can't go out of bounds, by design.\
This allows Rust to remove bounds checks from the generated machine code, making iteration faster.
In other words,
@@ -103,5 +103,5 @@ for i in 0..v.len() {
```
There are exceptions to this rule: the compiler can sometimes prove that you're not going out of bounds even
with manual indexing, thus removing the bounds checks anyway. But in general, prefer iteration to indexing
with manual indexing, thus removing the bounds checks anyway. But in general, prefer iteration to indexing
where possible.

View File

@@ -1,6 +1,6 @@
# `.iter()`
`IntoIterator` **consumes** `self` to create an iterator.
`IntoIterator` **consumes** `self` to create an iterator.
This has its benefits: you get **owned** values from the iterator.
For example: if you call `.into_iter()` on a `Vec<Ticket>` you'll get an iterator that returns `Ticket` values.
@@ -21,7 +21,7 @@ for n in numbers.iter() {
```
This pattern can be simplified by implementing `IntoIterator` for a **reference to the collection**.
In our example above, that would be `&Vec<Ticket>`.
In our example above, that would be `&Vec<Ticket>`.\
The standard library does this, that's why the following code works:
```rust
@@ -39,4 +39,4 @@ It's idiomatic to provide both options:
- An implementation of `IntoIterator` for a reference to the collection.
- An `.iter()` method that returns an iterator over references to the collection's elements.
The former is convenient in `for` loops, the latter is more explicit and can be used in other contexts.
The former is convenient in `for` loops, the latter is more explicit and can be used in other contexts.

View File

@@ -1,6 +1,6 @@
# Lifetimes
Let's try to complete the previous exercise by adding an implementation of `IntoIterator` for `&TicketStore`, for
Let's try to complete the previous exercise by adding an implementation of `IntoIterator` for `&TicketStore`, for
maximum convenience in `for` loops.
Let's start by filling in the most "obvious" parts of the implementation:
@@ -16,8 +16,8 @@ impl IntoIterator for &TicketStore {
}
```
What should `type IntoIter` be set to?
Intuitively, it should be the type returned by `self.tickets.iter()`, i.e. the type returned by `Vec::iter()`.
What should `type IntoIter` be set to?\
Intuitively, it should be the type returned by `self.tickets.iter()`, i.e. the type returned by `Vec::iter()`.\
If you check the standard library documentation, you'll find that `Vec::iter()` returns an `std::slice::Iter`.
The definition of `Iter` is:
@@ -29,8 +29,8 @@ pub struct Iter<'a, T> { /* fields omitted */ }
## Lifetime parameters
Lifetimes are **labels** used by the Rust compiler to keep track of how long a reference (either mutable or
immutable) is valid.
Lifetimes are **labels** used by the Rust compiler to keep track of how long a reference (either mutable or
immutable) is valid.\
The lifetime of a reference is constrained by the scope of the value it refers to. Rust always makes sure, at compile-time,
that references are not used after the value they refer to has been dropped, to avoid dangling pointers and use-after-free bugs.
@@ -49,8 +49,8 @@ impl <T> Vec<T> {
}
```
`Vec::iter()` is generic over a lifetime parameter, named `'a`.
`'a` is used to **tie together** the lifetime of the `Vec` and the lifetime of the `Iter` returned by `iter()`.
`Vec::iter()` is generic over a lifetime parameter, named `'a`.\
`'a` is used to **tie together** the lifetime of the `Vec` and the lifetime of the `Iter` returned by `iter()`.
In plain English: the `Iter` returned by `iter()` cannot outlive the `Vec` reference (`&self`) it was created from.
This is important because `Vec::iter`, as we discussed, returns an iterator over **references** to the `Vec`'s elements.
@@ -74,11 +74,11 @@ No explicit lifetime parameter is present in the signature of `Vec::iter()`.
Elision rules imply that the lifetime of the `Iter` returned by `iter()` is tied to the lifetime of the `&self` reference.
You can think of `'_` as a **placeholder** for the lifetime of the `&self` reference.
See the [References](#references) section for a link to the official documentation on lifetime elision.
See the [References](#references) section for a link to the official documentation on lifetime elision.\
In most cases, you can rely on the compiler telling you when you need to add explicit lifetime annotations.
## References
- [std::vec::Vec::iter](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.iter)
- [std::slice::Iter](https://doc.rust-lang.org/std/slice/struct.Iter.html)
- [Lifetime elision rules](https://doc.rust-lang.org/reference/lifetime-elision.html)
- [Lifetime elision rules](https://doc.rust-lang.org/reference/lifetime-elision.html)

View File

@@ -1,7 +1,7 @@
# Combinators
Iterators can do so much more than `for` loops!
If you look at the documentation for the `Iterator` trait, you'll find a **vast** collections of
Iterators can do so much more than `for` loops!\
If you look at the documentation for the `Iterator` trait, you'll find a **vast** collections of
methods that you can leverage to transform, filter, and combine iterators in various ways.
Let's mention the most common ones:
@@ -15,7 +15,7 @@ Let's mention the most common ones:
- `take` stops the iterator after `n` elements.
- `chain` combines two iterators into one.
These methods are called **combinators**.
These methods are called **combinators**.\
They are usually **chained** together to create complex transformations in a concise and readable way:
```rust
@@ -29,10 +29,10 @@ let outcome: u32 = numbers.iter()
## Closures
What's going on with the `filter` and `map` methods above?
What's going on with the `filter` and `map` methods above?\
They take **closures** as arguments.
Closures are **anonymous functions**, i.e. functions that are not defined using the `fn` syntax we are used to.
Closures are **anonymous functions**, i.e. functions that are not defined using the `fn` syntax we are used to.\
They are defined using the `|args| body` syntax, where `args` are the arguments and `body` is the function body.
`body` can be a block of code or a single expression.
For example:
@@ -70,10 +70,10 @@ let add_one: fn(i32) -> i32 = |x| x + 1;
## `collect`
What happens when you're done transforming an iterator using combinators?
What happens when you're done transforming an iterator using combinators?\
You either iterate over the transformed values using a `for` loop, or you collect them into a collection.
The latter is done using the `collect` method.
The latter is done using the `collect` method.\
`collect` consumes the iterator and collects its elements into a collection of your choice.
For example, you can collect the squares of the even numbers into a `Vec`:
@@ -86,7 +86,7 @@ let squares_of_evens: Vec<u32> = numbers.iter()
.collect();
```
`collect` is generic over its **return type**.
`collect` is generic over its **return type**.\
Therefore you usually need to provide a type hint to help the compiler infer the correct type.
In the example above, we annotated the type of `squares_of_evens` to be `Vec<u32>`.
Alternatively, you can use the **turbofish syntax** to specify the type:
@@ -104,4 +104,4 @@ let squares_of_evens = numbers.iter()
- [`Iterator`'s documentation](https://doc.rust-lang.org/std/iter/trait.Iterator.html) gives you an
overview of the methods available for iterators in `std`.
- [The `itertools` crate](https://docs.rs/itertools/) defines even **more** combinators for iterators.
- [The `itertools` crate](https://docs.rs/itertools/) defines even **more** combinators for iterators.

View File

@@ -1,6 +1,6 @@
# `impl Trait`
`TicketStore::to_dos` returns a `Vec<&Ticket>`.
`TicketStore::to_dos` returns a `Vec<&Ticket>`.\
That signature introduces a new heap allocation every time `to_dos` is called, which may be unnecessary depending
on what the caller needs to do with the result.
It'd be better if `to_dos` returned an iterator instead of a `Vec`, thus empowering the caller to decide whether to
@@ -25,9 +25,9 @@ The `filter` method returns an instance of `std::iter::Filter`, which has the fo
pub struct Filter<I, P> { /* fields omitted */ }
```
where `I` is the type of the iterator being filtered on and `P` is the predicate used to filter the elements.
We know that `I` is `std::slice::Iter<'_, Ticket>` in this case, but what about `P`?
`P` is a closure, an **anonymous function**. As the name suggests, closures don't have a name,
where `I` is the type of the iterator being filtered on and `P` is the predicate used to filter the elements.\
We know that `I` is `std::slice::Iter<'_, Ticket>` in this case, but what about `P`?\
`P` is a closure, an **anonymous function**. As the name suggests, closures don't have a name,
so we can't write them down in our code.
Rust has a solution for this: **impl Trait**.
@@ -51,19 +51,19 @@ That's it!
## Generic?
`impl Trait` in return position is **not** a generic parameter.
`impl Trait` in return position is **not** a generic parameter.
Generics are placeholders for types that are filled in by the caller of the function.
A function with a generic parameter is **polymorphic**: it can be called with different types, and the compiler will generate
a different implementation for each type.
That's not the case with `impl Trait`.
The return type of a function with `impl Trait` is **fixed** at compile time, and the compiler will generate
The return type of a function with `impl Trait` is **fixed** at compile time, and the compiler will generate
a single implementation for it.
This is why `impl Trait` is also called **opaque return type**: the caller doesn't know the exact type of the return value,
only that it implements the specified trait(s). But the compiler knows the exact type, there is no polymorphism involved.
## RPIT
If you read RFCs or deep-dives about Rust, you might come across the acronym **RPIT**.
It stands for **"Return Position Impl Trait"** and refers to the use of `impl Trait` in return position.
If you read RFCs or deep-dives about Rust, you might come across the acronym **RPIT**.\
It stands for **"Return Position Impl Trait"** and refers to the use of `impl Trait` in return position.

View File

@@ -1,6 +1,6 @@
# `impl Trait` in argument position
In the previous section, we saw how `impl Trait` can be used to return a type without specifying its name.
In the previous section, we saw how `impl Trait` can be used to return a type without specifying its name.\
The same syntax can also be used in **argument position**:
```rust
@@ -11,7 +11,7 @@ fn print_iter(iter: impl Iterator<Item = i32>) {
}
```
`print_iter` takes an iterator of `i32`s and prints each element.
`print_iter` takes an iterator of `i32`s and prints each element.\
When used in **argument position**, `impl Trait` is equivalent to a generic parameter with a trait bound:
```rust
@@ -27,6 +27,6 @@ where
## Downsides
As a rule of thumb, prefer generics over `impl Trait` in argument position.
Generics allow the caller to explicitly specify the type of the argument, using the turbofish syntax (`::<>`),
which can be useful for disambiguation. That's not the case with `impl Trait`.
As a rule of thumb, prefer generics over `impl Trait` in argument position.\
Generics allow the caller to explicitly specify the type of the argument, using the turbofish syntax (`::<>`),
which can be useful for disambiguation. That's not the case with `impl Trait`.

View File

@@ -21,12 +21,12 @@ Heap: | 1 | 2 | ? |
+---+---+---+
```
We already remarked how `String` is just a `Vec<u8>` in disguise.
We already remarked how `String` is just a `Vec<u8>` in disguise.\
The similarity should prompt you to ask: "What's the equivalent of `&str` for `Vec`?"
## `&[T]`
`[T]` is a **slice** of a contiguous sequence of elements of type `T`.
`[T]` is a **slice** of a contiguous sequence of elements of type `T`.\
It's most commonly used in its borrowed form, `&[T]`.
There are various ways to create a slice reference from a `Vec`:
@@ -54,7 +54,7 @@ let sum: i32 = numbers.iter().sum();
### Memory layout
A `&[T]` is a **fat pointer**, just like `&str`.
A `&[T]` is a **fat pointer**, just like `&str`.\
It consists of a pointer to the first element of the slice and the length of the slice.
If you have a `Vec` with three elements:
@@ -90,10 +90,10 @@ Heap: | 1 | 2 | 3 | ? | |
### `&Vec<T>` vs `&[T]`
When you need to pass an immutable reference to a `Vec` to a function, prefer `&[T]` over `&Vec<T>`.
When you need to pass an immutable reference to a `Vec` to a function, prefer `&[T]` over `&Vec<T>`.\
This allows the function to accept any kind of slice, not necessarily one backed by a `Vec`.
For example, you can then pass a subset of the elements in a `Vec`.
For example, you can then pass a subset of the elements in a `Vec`.
But it goes further than that—you could also pass a **slice of an array**:
```rust
@@ -102,5 +102,5 @@ let slice: &[i32] = &array;
```
Array slices and `Vec` slices are the same type: they're fat pointers to a contiguous sequence of elements.
In the case of arrays, the pointer points to the stack rather than the heap, but that doesn't matter
when it comes to using the slice.
In the case of arrays, the pointer points to the stack rather than the heap, but that doesn't matter
when it comes to using the slice.

View File

@@ -1,6 +1,6 @@
# Mutable slices
Every time we've talked about slice types (like `str` and `[T]`), we've used their immutable borrow form (`&str` and `&[T]`).
Every time we've talked about slice types (like `str` and `[T]`), we've used their immutable borrow form (`&str` and `&[T]`).\
But slices can also be mutable!
Here's how you create a mutable slice:
@@ -21,7 +21,7 @@ This will change the first element of the `Vec` to `42`.
## Limitations
When working with immutable borrows, the recommendation was clear: prefer slice references over references to
the owned type (e.g. `&[T]` over `&Vec<T>`).
the owned type (e.g. `&[T]` over `&Vec<T>`).\
That's **not** the case with mutable borrows.
Consider this scenario:
@@ -32,10 +32,10 @@ let mut slice: &mut [i32] = &mut numbers;
slice.push(1);
```
It won't compile!
`push` is a method on `Vec`, not on slices. This is the manifestation of a more general principle: Rust won't
allow you to add or remove elements from a slice. You will only be able to modify/replace the elements that are
It won't compile!\
`push` is a method on `Vec`, not on slices. This is the manifestation of a more general principle: Rust won't
allow you to add or remove elements from a slice. You will only be able to modify/replace the elements that are
already there.
In this regard, a `&mut Vec` or a `&mut String` are strictly more powerful than a `&mut [T]` or a `&mut str`.
Choose the type that best fits based on the operations you need to perform.
In this regard, a `&mut Vec` or a `&mut String` are strictly more powerful than a `&mut [T]` or a `&mut str`.\
Choose the type that best fits based on the operations you need to perform.

View File

@@ -1,6 +1,6 @@
# Ticket ids
Let's think again about our ticket management system.
Let's think again about our ticket management system.\
Our ticket model right now looks like this:
```rust
@@ -11,13 +11,13 @@ pub struct Ticket {
}
```
One thing is missing here: an **identifier** to uniquely identify a ticket.
That identifier should be unique for each ticket. That can be guaranteed by generating it automatically when
One thing is missing here: an **identifier** to uniquely identify a ticket.\
That identifier should be unique for each ticket. That can be guaranteed by generating it automatically when
a new ticket is created.
## Refining the model
Where should the id be stored?
Where should the id be stored?\
We could add a new field to the `Ticket` struct:
```rust
@@ -29,7 +29,7 @@ pub struct Ticket {
}
```
But we don't know the id before creating the ticket. So it can't be there from the get-go.
But we don't know the id before creating the ticket. So it can't be there from the get-go.\
It'd have to be optional:
```rust
@@ -61,7 +61,7 @@ pub struct Ticket {
}
```
A `TicketDraft` is a ticket that hasn't been created yet. It doesn't have an id, and it doesn't have a status.
A `Ticket` is a ticket that has been created. It has an id and a status.
A `TicketDraft` is a ticket that hasn't been created yet. It doesn't have an id, and it doesn't have a status.\
A `Ticket` is a ticket that has been created. It has an id and a status.\
Since each field in `TicketDraft` and `Ticket` embeds its own constraints, we don't have to duplicate logic
across the two types.
across the two types.

View File

@@ -1,7 +1,7 @@
# Indexing
`TicketStore::get` returns an `Option<&Ticket>` for a given `TicketId`.
We've seen before how to access elements of arrays and vectors using Rust's
`TicketStore::get` returns an `Option<&Ticket>` for a given `TicketId`.\
We've seen before how to access elements of arrays and vectors using Rust's
indexing syntax:
```rust
@@ -9,7 +9,7 @@ let v = vec![0, 1, 2];
assert_eq!(v[0], 0);
```
How can we provide the same experience for `TicketStore`?
How can we provide the same experience for `TicketStore`?\
You guessed right: we need to implement a trait, `Index`!
## `Index`
@@ -34,4 +34,4 @@ It has:
Notice how the `index` method doesn't return an `Option`. The assumption is that
`index` will panic if you try to access an element that's not there, as it happens
for array and vec indexing.
for array and vec indexing.

View File

@@ -1,6 +1,6 @@
# Mutable indexing
`Index` allows read-only access. It doesn't let you mutate the value you
`Index` allows read-only access. It doesn't let you mutate the value you
retrieved.
## `IndexMut`
@@ -17,4 +17,4 @@ pub trait IndexMut<Idx>: Index<Idx>
```
`IndexMut` can only be implemented if the type already implements `Index`,
since it unlocks an _additional_ capability.
since it unlocks an _additional_ capability.

View File

@@ -2,7 +2,7 @@
Our implementation of `Index`/`IndexMut` is not ideal: we need to iterate over the entire
`Vec` to retrieve a ticket by id; the algorithmic complexity is `O(n)`, where
`n` is the number of tickets in the store.
`n` is the number of tickets in the store.
We can do better by using a different data structure for storing tickets: a `HashMap<K, V>`.
@@ -20,7 +20,7 @@ book_reviews.insert(
```
`HashMap` works with key-value pairs. It's generic over both: `K` is the generic
parameter for the key type, while `V` is the one for the value type.
parameter for the key type, while `V` is the one for the value type.
The expected cost of insertions, retrievals and removals is **constant**, `O(1)`.
That sounds perfect for our usecase, doesn't it?
@@ -42,17 +42,17 @@ where
}
```
The key type must implement the `Eq` and `Hash` traits.
The key type must implement the `Eq` and `Hash` traits.\
Let's dig into those two.
## `Hash`
A hashing function (or hasher) maps a potentially infinite set of a values (e.g.
all possible strings) to a bounded range (e.g. a `u64` value).
There are many different hashing functions around, each with different properties
all possible strings) to a bounded range (e.g. a `u64` value).\
There are many different hashing functions around, each with different properties
(speed, collision risk, reversibility, etc.).
A `HashMap`, as the name suggests, uses a hashing function behind the scene.
A `HashMap`, as the name suggests, uses a hashing function behind the scene.
It hashes your key and then uses that hash to store/retrieve the associated value.
This strategy requires the key type must be hashable, hence the `Hash` trait bound on `K`.
@@ -81,10 +81,10 @@ struct Person {
`HashMap` must be able to compare keys for equality. This is particularly important
when dealing with hash collisions—i.e. when two different keys hash to the same value.
You may wonder: isn't that what the `PartialEq` trait is for? Almost!
`PartialEq` is not enough for `HashMap` because it doesn't guarantee reflexivity, i.e. `a == a` is always `true`.
For example, floating point numbers (`f32` and `f64`) implement `PartialEq`,
but they don't satisfy the reflexivity property: `f32::NAN == f32::NAN` is `false`.
You may wonder: isn't that what the `PartialEq` trait is for? Almost!\
`PartialEq` is not enough for `HashMap` because it doesn't guarantee reflexivity, i.e. `a == a` is always `true`.\
For example, floating point numbers (`f32` and `f64`) implement `PartialEq`,
but they don't satisfy the reflexivity property: `f32::NAN == f32::NAN` is `false`.\
Reflexivity is crucial for `HashMap` to work correctly: without it, you wouldn't be able to retrieve a value
from the map using the same key you used to insert it.
@@ -97,7 +97,7 @@ pub trait Eq: PartialEq {
```
It's a marker trait: it doesn't add any new methods, it's just a way for you to say to the compiler
that the equality logic implemented in `PartialEq` is reflexive.
that the equality logic implemented in `PartialEq` is reflexive.
You can derive `Eq` automatically when you derive `PartialEq`:
@@ -113,4 +113,4 @@ struct Person {
There is an implicit contract between `Eq` and `Hash`: if two keys are equal, their hashes must be equal too.
This is crucial for `HashMap` to work correctly. If you break this contract, you'll get nonsensical results
when using `HashMap`.
when using `HashMap`.

View File

@@ -1,20 +1,20 @@
# Ordering
By moving from a `Vec` to a `HashMap` we have improved the performance of our ticket management system,
and simplified our code in the process.
It's not all roses, though. When iterating over a `Vec`-backed store, we could be sure that the tickets
would be returned in the order they were added.
and simplified our code in the process.\
It's not all roses, though. When iterating over a `Vec`-backed store, we could be sure that the tickets
would be returned in the order they were added.\
That's not the case with a `HashMap`: you can iterate over the tickets, but the order is random.
We can recover a consistent ordering by switching from a `HashMap` to a `BTreeMap`.
## `BTreeMap`
A `BTreeMap` guarantees that entries are sorted by their keys.
A `BTreeMap` guarantees that entries are sorted by their keys.\
This is useful when you need to iterate over the entries in a specific order, or if you need to
perform range queries (e.g. "give me all tickets with an id between 10 and 20").
Just like `HashMap`, you won't find trait bounds on the definition of `BTreeMap`.
Just like `HashMap`, you won't find trait bounds on the definition of `BTreeMap`.
But you'll find trait bounds on its methods. Let's look at `insert`:
```rust
@@ -34,7 +34,7 @@ impl<K, V> BTreeMap<K, V> {
## `Ord`
The `Ord` trait is used to compare values.
The `Ord` trait is used to compare values.\
While `PartialEq` is used to compare for equality, `Ord` is used to compare for ordering.
It's defined in `std::cmp`:
@@ -45,8 +45,8 @@ pub trait Ord: Eq + PartialOrd {
}
```
The `cmp` method returns an `Ordering` enum, which can be one
of `Less`, `Equal`, or `Greater`.
The `cmp` method returns an `Ordering` enum, which can be one
of `Less`, `Equal`, or `Greater`.\
`Ord` requires that two other traits are implemented: `Eq` and `PartialOrd`.
## `PartialOrd`
@@ -60,9 +60,9 @@ pub trait PartialOrd: PartialEq {
}
```
`PartialOrd::partial_cmp` returns an `Option`—it is not guaranteed that two values can
be compared.
For example, `f32` doesn't implement `Ord` because `NaN` values are not comparable,
`PartialOrd::partial_cmp` returns an `Option`—it is not guaranteed that two values can
be compared.\
For example, `f32` doesn't implement `Ord` because `NaN` values are not comparable,
the same reason why `f32` doesn't implement `Eq`.
## Implementing `Ord` and `PartialOrd`
@@ -79,4 +79,4 @@ struct TicketId(u64);
If you choose (or need) to implement them manually, be careful:
- `Ord` and `PartialOrd` must be consistent with `Eq` and `PartialEq`.
- `Ord` and `PartialOrd` must be consistent with each other.
- `Ord` and `PartialOrd` must be consistent with each other.