diff --git a/Cargo.lock b/Cargo.lock index c572c57..5b93b94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/book/src/04_traits/05_trait_bounds.md b/book/src/04_traits/05_trait_bounds.md new file mode 100644 index 0000000..9c3a50c --- /dev/null +++ b/book/src/04_traits/05_trait_bounds.md @@ -0,0 +1,154 @@ +# 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 that works with a **type parameter** instead of a concrete type: + +```rust +fn print_if_even(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(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(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(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(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(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. diff --git a/book/src/04_traits/05_str_slice.md b/book/src/04_traits/06_str_slice.md similarity index 100% rename from book/src/04_traits/05_str_slice.md rename to book/src/04_traits/06_str_slice.md diff --git a/book/src/04_traits/06_deref.md b/book/src/04_traits/07_deref.md similarity index 100% rename from book/src/04_traits/06_deref.md rename to book/src/04_traits/07_deref.md diff --git a/book/src/04_traits/07_sized.md b/book/src/04_traits/08_sized.md similarity index 100% rename from book/src/04_traits/07_sized.md rename to book/src/04_traits/08_sized.md diff --git a/book/src/04_traits/08_from.md b/book/src/04_traits/09_from.md similarity index 81% rename from book/src/04_traits/08_from.md rename to book/src/04_traits/09_from.md index ee48205..4048955 100644 --- a/book/src/04_traits/08_from.md +++ b/book/src/04_traits/09_from.md @@ -39,8 +39,8 @@ pub trait Into: 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 { is actually equivalent to: ```rust -pub struct Foo -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 traits to be implemented using -// the `+` sign. E.g. `Sized + PartialEq` +pub struct Foo { 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 { // ^^^^^^^ // This is a negative trait bound @@ -97,7 +80,8 @@ pub struct Foo { ``` This syntax reads as "`T` may or may not be `Sized`", and it allows you to -bind `T` to a DST (e.g. `Foo`). +bind `T` to a DST (e.g. `Foo`). 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`, we want _both_ `T` and the type implementing `From` to be `Sized`, even though the former bound is implicit. diff --git a/book/src/04_traits/09_assoc_vs_generic.md b/book/src/04_traits/10_assoc_vs_generic.md similarity index 100% rename from book/src/04_traits/09_assoc_vs_generic.md rename to book/src/04_traits/10_assoc_vs_generic.md diff --git a/book/src/04_traits/10_clone.md b/book/src/04_traits/11_clone.md similarity index 100% rename from book/src/04_traits/10_clone.md rename to book/src/04_traits/11_clone.md diff --git a/book/src/04_traits/11_copy.md b/book/src/04_traits/12_copy.md similarity index 100% rename from book/src/04_traits/11_copy.md rename to book/src/04_traits/12_copy.md diff --git a/book/src/04_traits/12_drop.md b/book/src/04_traits/13_drop.md similarity index 100% rename from book/src/04_traits/12_drop.md rename to book/src/04_traits/13_drop.md diff --git a/book/src/04_traits/13_outro.md b/book/src/04_traits/14_outro.md similarity index 100% rename from book/src/04_traits/13_outro.md rename to book/src/04_traits/14_outro.md diff --git a/exercises/04_traits/05_trait_bounds/Cargo.toml b/exercises/04_traits/05_trait_bounds/Cargo.toml new file mode 100644 index 0000000..a9a51d1 --- /dev/null +++ b/exercises/04_traits/05_trait_bounds/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "trait_bounds" +version = "0.1.0" +edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/05_trait_bounds/src/lib.rs b/exercises/04_traits/05_trait_bounds/src/lib.rs new file mode 100644 index 0000000..f3d87c0 --- /dev/null +++ b/exercises/04_traits/05_trait_bounds/src/lib.rs @@ -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(left: T, right: T) -> T { + if left <= right { + left + } else { + right + } +} diff --git a/exercises/04_traits/05_str_slice/Cargo.toml b/exercises/04_traits/06_str_slice/Cargo.toml similarity index 100% rename from exercises/04_traits/05_str_slice/Cargo.toml rename to exercises/04_traits/06_str_slice/Cargo.toml diff --git a/exercises/04_traits/05_str_slice/src/lib.rs b/exercises/04_traits/06_str_slice/src/lib.rs similarity index 100% rename from exercises/04_traits/05_str_slice/src/lib.rs rename to exercises/04_traits/06_str_slice/src/lib.rs diff --git a/exercises/04_traits/06_deref/Cargo.toml b/exercises/04_traits/07_deref/Cargo.toml similarity index 100% rename from exercises/04_traits/06_deref/Cargo.toml rename to exercises/04_traits/07_deref/Cargo.toml diff --git a/exercises/04_traits/06_deref/src/lib.rs b/exercises/04_traits/07_deref/src/lib.rs similarity index 100% rename from exercises/04_traits/06_deref/src/lib.rs rename to exercises/04_traits/07_deref/src/lib.rs diff --git a/exercises/04_traits/07_sized/Cargo.toml b/exercises/04_traits/08_sized/Cargo.toml similarity index 100% rename from exercises/04_traits/07_sized/Cargo.toml rename to exercises/04_traits/08_sized/Cargo.toml diff --git a/exercises/04_traits/07_sized/src/lib.rs b/exercises/04_traits/08_sized/src/lib.rs similarity index 100% rename from exercises/04_traits/07_sized/src/lib.rs rename to exercises/04_traits/08_sized/src/lib.rs diff --git a/exercises/04_traits/08_from/Cargo.toml b/exercises/04_traits/09_from/Cargo.toml similarity index 100% rename from exercises/04_traits/08_from/Cargo.toml rename to exercises/04_traits/09_from/Cargo.toml diff --git a/exercises/04_traits/08_from/src/lib.rs b/exercises/04_traits/09_from/src/lib.rs similarity index 100% rename from exercises/04_traits/08_from/src/lib.rs rename to exercises/04_traits/09_from/src/lib.rs diff --git a/exercises/04_traits/09_assoc_vs_generic/Cargo.toml b/exercises/04_traits/10_assoc_vs_generic/Cargo.toml similarity index 100% rename from exercises/04_traits/09_assoc_vs_generic/Cargo.toml rename to exercises/04_traits/10_assoc_vs_generic/Cargo.toml diff --git a/exercises/04_traits/09_assoc_vs_generic/src/lib.rs b/exercises/04_traits/10_assoc_vs_generic/src/lib.rs similarity index 100% rename from exercises/04_traits/09_assoc_vs_generic/src/lib.rs rename to exercises/04_traits/10_assoc_vs_generic/src/lib.rs diff --git a/exercises/04_traits/10_clone/Cargo.toml b/exercises/04_traits/11_clone/Cargo.toml similarity index 100% rename from exercises/04_traits/10_clone/Cargo.toml rename to exercises/04_traits/11_clone/Cargo.toml diff --git a/exercises/04_traits/10_clone/src/lib.rs b/exercises/04_traits/11_clone/src/lib.rs similarity index 100% rename from exercises/04_traits/10_clone/src/lib.rs rename to exercises/04_traits/11_clone/src/lib.rs diff --git a/exercises/04_traits/11_copy/Cargo.toml b/exercises/04_traits/12_copy/Cargo.toml similarity index 100% rename from exercises/04_traits/11_copy/Cargo.toml rename to exercises/04_traits/12_copy/Cargo.toml diff --git a/exercises/04_traits/11_copy/src/lib.rs b/exercises/04_traits/12_copy/src/lib.rs similarity index 100% rename from exercises/04_traits/11_copy/src/lib.rs rename to exercises/04_traits/12_copy/src/lib.rs diff --git a/exercises/04_traits/12_drop/Cargo.toml b/exercises/04_traits/13_drop/Cargo.toml similarity index 100% rename from exercises/04_traits/12_drop/Cargo.toml rename to exercises/04_traits/13_drop/Cargo.toml diff --git a/exercises/04_traits/12_drop/src/lib.rs b/exercises/04_traits/13_drop/src/lib.rs similarity index 100% rename from exercises/04_traits/12_drop/src/lib.rs rename to exercises/04_traits/13_drop/src/lib.rs diff --git a/exercises/04_traits/13_outro/Cargo.toml b/exercises/04_traits/14_outro/Cargo.toml similarity index 100% rename from exercises/04_traits/13_outro/Cargo.toml rename to exercises/04_traits/14_outro/Cargo.toml diff --git a/exercises/04_traits/13_outro/src/lib.rs b/exercises/04_traits/14_outro/src/lib.rs similarity index 100% rename from exercises/04_traits/13_outro/src/lib.rs rename to exercises/04_traits/14_outro/src/lib.rs diff --git a/exercises/04_traits/13_outro/tests/integration.rs b/exercises/04_traits/14_outro/tests/integration.rs similarity index 100% rename from exercises/04_traits/13_outro/tests/integration.rs rename to exercises/04_traits/14_outro/tests/integration.rs