Compare commits

...

8 Commits

Author SHA1 Message Date
Onè
20ff3a1743 Add missing word (#45) 2024-05-24 10:28:27 +02:00
Shinya Fujino
d2be52f32f Update references in book/src/04_traits (#46) 2024-05-24 10:27:29 +02:00
LukeMathWalker
46e2dcb2b9 Fix index. 2024-05-23 16:33:55 +02:00
LukeMathWalker
453d8030e5 Add new section on trait bounds. 2024-05-23 15:29:42 +02:00
LukeMathWalker
2477f72adc Remove ambiguity in 03/07 exercise mandate. 2024-05-23 14:39:43 +02:00
LukeMathWalker
f645b500c4 Improve as casting exercise. 2024-05-23 14:37:05 +02:00
Onè
bf1cdfdb5c reword tuples introduction (#42) 2024-05-23 14:29:55 +02:00
Onè
aecd6e6180 typos (#41)
* that to than

* add missing s

* an to a

Next letter is a consonant sound
2024-05-23 14:29:37 +02:00
37 changed files with 231 additions and 48 deletions

4
Cargo.lock generated
View File

@@ -768,6 +768,10 @@ dependencies = [
name = "trait_"
version = "0.1.0"
[[package]]
name = "trait_bounds"
version = "0.1.0"
[[package]]
name = "tryfrom"
version = "0.1.0"

View File

@@ -0,0 +1,158 @@
# Trait bounds
We've seen two use cases for traits so far:
- Unlocking "built-in" behaviour (e.g. operator overloading)
- Adding new behaviour to existing types (i.e. extension traits)
There's a third use case: **generic programming**.
## The problem
All our functions and methods, so far, have been working with **concrete types**.
Code that operates on concrete types is usually straightforward to write and understand. But it's also
limited in its reusability.
Let's imagine, for example, that we want to write a function that returns `true` if an integer is even.
Working with concrete types, we'd have to write a separate function for each integer type we want to
support:
```rust
fn is_even_i32(n: i32) -> bool {
n % 2 == 0
}
fn is_even_i64(n: i64) -> bool {
n % 2 == 0
}
// Etc.
```
Alternatively, we could write a single extension trait and then different implementations for each integer type:
```rust
trait IsEven {
fn is_even(&self) -> bool;
}
impl IsEven for i32 {
fn is_even(&self) -> bool {
self % 2 == 0
}
}
impl IsEven for i64 {
fn is_even(&self) -> bool {
self % 2 == 0
}
}
// Etc.
```
The duplication remains.
## Generic programming
We can do better using **generics**.
Generics allow us to write code that works with a **type parameter** instead of a concrete type:
```rust
fn print_if_even<T>(n: T)
where
T: IsEven + Debug
{
if n.is_even() {
println!("{n:?} is even");
}
}
```
`print_if_even` is a **generic function**.
It isn't tied to a specific input type. Instead, it works with any type `T` that:
- Implements the `IsEven` trait.
- Implements the `Debug` trait.
This contract is expressed with a **trait bound**: `T: IsEven + Debug`.
The `+` symbol is used to require that `T` implements multiple traits. `T: IsEven + Debug` is equivalent to
"where `T` implements `IsEven` **and** `Debug`".
## Trait bounds
What purpose do trait bounds serve in `print_if_even`?
To find out, let's try to remove them:
```rust
fn print_if_even<T>(n: T) {
if n.is_even() {
println!("{n:?} is even");
}
}
```
This code won't compile:
```text
error[E0599]: no method named `is_even` found for type parameter `T` in the current scope
--> src/lib.rs:2:10
|
1 | fn print_if_even<T>(n: T) {
| - method `is_even` not found for this type parameter
2 | if n.is_even() {
| ^^^^^^^ method not found in `T`
error[E0277]: `T` doesn't implement `Debug`
--> src/lib.rs:3:19
|
3 | println!("{n:?} is even");
| ^^^^^ `T` cannot be formatted using `{:?}` because it doesn't implement `Debug`
|
help: consider restricting type parameter `T`
|
1 | fn print_if_even<T: std::fmt::Debug>(n: T) {
| +++++++++++++++++
```
Without trait bounds, the compiler doesn't know what `T` **can do**.
It doesn't know that `T` has an `is_even` method, and it doesn't know how to format `T` for printing.
Trait bounds restrict the set of types that can be used by ensuring that the behaviour required by the function
body is present.
## Inlining trait bounds
All the examples above used a **`where` clause** to specify trait bounds:
```rust
fn print_if_even<T>(n: T)
where
T: IsEven + Debug
// ^^^^^^^^^^^^^^^^^
// This is a `where` clause
{
// [...]
}
```
If the trait bounds are simple, you can **inline** them directly next to the type parameter:
```rust
fn print_if_even<T: IsEven + Debug>(n: T) {
// ^^^^^^^^^^^^^^^^^
// This is an inline trait bound
// [...]
}
```
## The function signature is king
You may wonder why we need trait bounds at all. Can't the compiler infer the required traits from the function's body?
It could, but it won't.
The rationale is the same as for [explicit type annotations on function parameters](../02_basic_calculator/02_variables#function-arguments-are-variables):
each function signature is a contract between the caller and the callee, and the terms must be explicitly stated.
This allows for better error messages, better documentation, less unintentional breakages across versions,
and faster compilation times.
## References
- The exercise for this section is located in `exercises/04_traits/05_trait_bounds`

View File

@@ -117,4 +117,4 @@ bunch of text data and that a subset of it matches what you need, therefore you'
## References
- The exercise for this section is located in `exercises/04_traits/05_str_slice`
- The exercise for this section is located in `exercises/04_traits/06_str_slice`

View File

@@ -92,4 +92,4 @@ We'll examine later in the course the "safest" use cases for deref coercion: sma
## References
- The exercise for this section is located in `exercises/04_traits/06_deref`
- The exercise for this section is located in `exercises/04_traits/07_deref`

View File

@@ -1,6 +1,6 @@
# `Sized`
There's more to `&str` that meets the eye, even after having
There's more to `&str` than meets the eye, even after having
investigated deref coercion.
From our previous [discussion on memory layouts](../03_ticket_v1/10_references_in_memory.md),
it would have been reasonable to expect `&str` to be represented as a single `usize` on
@@ -80,4 +80,4 @@ and one for the length.
## References
- The exercise for this section is located in `exercises/04_traits/07_sized`
- The exercise for this section is located in `exercises/04_traits/08_sized`

View File

@@ -39,8 +39,8 @@ pub trait Into<T>: Sized {
}
```
These trait definitions showcase a few concepts that we haven't seen before: **supertraits**, **generics**,
and **implicit trait bounds**. Let's unpack those first.
These trait definitions showcase a few concepts that we haven't seen before: **supertraits** and **implicit trait bounds**.
Let's unpack those first.
### Supertrait / Subtrait
@@ -48,12 +48,6 @@ The `From: Sized` syntax implies that `From` is a **subtrait** of `Sized`: any t
implements `From` must also implement `Sized`.
Alternatively, you could say that `Sized` is a **supertrait** of `From`.
### Generics
Both `From` and `Into` are **generic traits**.
They take a type parameter, `T`, to refer to the type being converted from or into.
`T` is a placeholder for the actual type, which will be specified when the trait is implemented or used.
### Implicit trait bounds
Every time you have a generic type parameter, the compiler implicitly assumes that it's `Sized`.
@@ -69,15 +63,7 @@ pub struct Foo<T> {
is actually equivalent to:
```rust
pub struct Foo<T>
where
T: Sized,
// ^^^^^^^^^
// This is known as a **trait bound**
// It specifies that this implementation applies exclusively
// to types `T` that implement `Sized`
// You can require multiple trait to be implemented using
// the `+` sign. E.g. `Sized + PartialEq<T>`
pub struct Foo<T: Sized>
{
inner: T,
}
@@ -86,9 +72,6 @@ where
You can opt out of this behavior by using a **negative trait bound**:
```rust
// You can also choose to inline trait bounds,
// rather than using `where` clauses
pub struct Foo<T: ?Sized> {
// ^^^^^^^
// This is a negative trait bound
@@ -97,7 +80,8 @@ pub struct Foo<T: ?Sized> {
```
This syntax reads as "`T` may or may not be `Sized`", and it allows you to
bind `T` to a DST (e.g. `Foo<str>`).
bind `T` to a DST (e.g. `Foo<str>`). It is a special case, though: negative trait bounds are exclusive to `Sized`,
you can't use them with other traits.
In the case of `From<T>`, we want _both_ `T` and the type implementing `From<T>` to be `Sized`, even
though the former bound is implicit.
@@ -146,4 +130,4 @@ In most cases, the target type is either:
## References
- The exercise for this section is located in `exercises/04_traits/08_from`
- The exercise for this section is located in `exercises/04_traits/09_from`

View File

@@ -74,7 +74,7 @@ It uses both mechanisms:
### `RHS`
`RHS` is a generic parameter to allow for different types to be added together.
For example, you'll find these two implementation in the standard library:
For example, you'll find these two implementations in the standard library:
```rust
impl Add<u32> for u32 {
@@ -115,4 +115,4 @@ To recap:
## References
- The exercise for this section is located in `exercises/04_traits/09_assoc_vs_generic`
- The exercise for this section is located in `exercises/04_traits/10_assoc_vs_generic`

View File

@@ -108,4 +108,4 @@ Remember that you can use `cargo expand` (or your IDE) to explore the code gener
## References
- The exercise for this section is located in `exercises/04_traits/10_clone`
- The exercise for this section is located in `exercises/04_traits/11_clone`

View File

@@ -114,4 +114,4 @@ struct MyStruct {
## References
- The exercise for this section is located in `exercises/04_traits/11_copy`
- The exercise for this section is located in `exercises/04_traits/12_copy`

View File

@@ -53,4 +53,4 @@ error[E0184]: the trait `Copy` cannot be implemented for this type; the type has
## References
- The exercise for this section is located in `exercises/04_traits/12_drop`
- The exercise for this section is located in `exercises/04_traits/13_drop`

View File

@@ -9,4 +9,4 @@ You'll have minimal guidance this time—just the exercise description and the t
## References
- The exercise for this section is located in `exercises/04_traits/13_outro`
- The exercise for this section is located in `exercises/04_traits/14_outro`

View File

@@ -23,7 +23,7 @@ impl Status {
```
A `match` statement that lets you compare a Rust value against a series of **patterns**.
You can think of it as a type-level `if`. If `status` is an `Done` variant, execute the first block;
You can think of it as a type-level `if`. If `status` is a `Done` variant, execute the first block;
if it's a `InProgress` or `ToDo` variant, execute the second block.
## Exhaustiveness

View File

@@ -52,7 +52,8 @@ let y = point.1;
### Tuples
It's weird say that something is tuple-like when we haven't seen tuples yet!
Tuples are Rust primitive types. They group together a fixed number of values with (potentially different) types:
Tuples are another example of a primitive Rust type.
They group together a fixed number of values with (potentially different) types:
```rust
// Two values, same type

View File

@@ -34,15 +34,16 @@
- [Orphan rule](04_traits/02_orphan_rule.md)
- [Operator overloading](04_traits/03_operator_overloading.md)
- [Derive macros](04_traits/04_derive.md)
- [String slices](04_traits/05_str_slice.md)
- [`Deref` trait](04_traits/06_deref.md)
- [`Sized` trait](04_traits/07_sized.md)
- [`From` trait](04_traits/08_from.md)
- [Associated vs generic types](04_traits/09_assoc_vs_generic.md)
- [`Clone` trait](04_traits/10_clone.md)
- [`Copy` trait](04_traits/11_copy.md)
- [`Drop` trait](04_traits/12_drop.md)
- [Outro](04_traits/13_outro.md)
- [Trait bounds](04_traits/05_trait_bounds.md)
- [String slices](04_traits/06_str_slice.md)
- [`Deref` trait](04_traits/07_deref.md)
- [`Sized` trait](04_traits/08_sized.md)
- [`From` trait](04_traits/09_from.md)
- [Associated vs generic types](04_traits/10_assoc_vs_generic.md)
- [`Clone` trait](04_traits/11_clone.md)
- [`Copy` trait](04_traits/12_copy.md)
- [`Drop` trait](04_traits/13_drop.md)
- [Outro](04_traits/14_outro.md)
- [Ticket v2](05_ticket_v2/00_intro.md)
- [Enums](05_ticket_v2/01_enum.md)

View File

@@ -6,17 +6,32 @@ mod tests {
#[test]
fn u16_to_u32() {
assert_eq!(47u16 as u32, todo!());
let v: u32 = todo!();
assert_eq!(47u16 as u32, v);
}
#[test]
#[allow(overflowing_literals)]
fn u8_to_i8() {
assert_eq!(255 as i8, todo!());
// The compiler is smart enough to know that the value 255 cannot fit
// inside an i8, so it'll emit a hard error. We intentionally disable
// this guardrail to make this (bad) conversion possible.
// The compiler is only able to pick on this because the value is a
// literal. If we were to use a variable, the compiler wouldn't be able to
// catch this at compile time.
#[allow(overflowing_literals)]
let x = { 255 as i8 };
// You could solve this by using exactly the same expression as above,
// but that would defeat the purpose of the exercise. Instead, use a genuine
// `i8` value that is equivalent to `255` when converted from `u8`.
let y: i8 = todo!();
assert_eq!(x, y);
}
#[test]
fn bool_to_u8() {
assert_eq!(true as u8, todo!());
let v: u8 = todo!();
assert_eq!(true as u8, v);
}
}

View File

@@ -1,6 +1,7 @@
// TODO: Add &mut-setters to the `Ticket` struct for each of its fields.
// Make sure to enforce the same validation rules you have in `Ticket::new`!
// Even better, extract that logic into private methods and reuse it in both places.
// Even better, extract that logic and reuse it in both places. You can use
// private functions or private static methods for that.
pub struct Ticket {
title: String,

View File

@@ -0,0 +1,4 @@
[package]
name = "trait_bounds"
version = "0.1.0"
edition = "2021"

View File

@@ -0,0 +1,15 @@
// TODO: Add the necessary trait bounds to `min` so that it compiles successfully.
// Refer to `std::cmp` for more information on the traits you might need.
//
// Note: there are different trait bounds that'll make the compiler happy, but they come with
// different _semantics_. We'll cover those differences later in the course when we talk about ordered
// collections (e.g. BTreeMap).
/// Return the minimum of two values.
pub fn min<T>(left: T, right: T) -> T {
if left <= right {
left
} else {
right
}
}