-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Extension traits #2812
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
Merged
Merged
Extension traits #2812
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
be796c3
Extension traits
LukeMathWalker 408962c
Reword
LukeMathWalker 1670b64
Use consistent terminology
LukeMathWalker bd1b26d
Extending other traits
LukeMathWalker 06f251e
Trait method conflicts
LukeMathWalker 21a105e
Shorter title
LukeMathWalker b64886c
Mark trait method conflict as compile_fail
LukeMathWalker c36f7fe
Fix link
LukeMathWalker 5ab37d8
Update src/idiomatic/leveraging-the-type-system/extension-traits/exte…
LukeMathWalker 841bce5
Update src/idiomatic/leveraging-the-type-system/extension-traits/meth…
LukeMathWalker aa2ab0f
Update src/idiomatic/leveraging-the-type-system/extension-traits/meth…
LukeMathWalker 63d3aa3
Update src/idiomatic/leveraging-the-type-system/extension-traits/meth…
LukeMathWalker 17ba065
Address review comments
LukeMathWalker d10c985
Formatting and typos
LukeMathWalker 771a37a
Elaborate further on the desired goal when extending other traits
LukeMathWalker b58a2a5
Extract bullet point into its own slide
LukeMathWalker d141a8d
Apply suggestions from code review
tall-vase a0146cc
Address feedback + grammar check
631e8f4
Merge branch 'main' into extension-traits
tall-vase 85a2e5f
Space out paragraphs
tall-vase 469a040
Address feedback wrt demonstration of trait method conflict slide
3d17013
Update src/idiomatic/leveraging-the-type-system/extension-traits/trai…
tall-vase 669c74a
Formatting pass
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
src/idiomatic/leveraging-the-type-system/extension-traits.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
--- | ||
minutes: 15 | ||
--- | ||
|
||
# Extension Traits | ||
|
||
It may desirable to **extend** foreign types with new inherent methods. For | ||
example, allow your code to check if a string is a palindrome using | ||
method-calling syntax: `s.is_palindrome()`. | ||
|
||
It might feel natural to reach out for an `impl` block: | ||
|
||
```rust,compile_fail | ||
randomPoison marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 🛠️❌ | ||
impl &'_ str { | ||
pub fn is_palindrome(&self) -> bool { | ||
self.chars().eq(self.chars().rev()) | ||
} | ||
} | ||
``` | ||
|
||
The Rust compiler won't allow it, though. But you can use the **extension trait | ||
pattern** to work around this limitation. | ||
|
||
<details> | ||
|
||
- A Rust item (be it a trait or a type) is referred to as: | ||
|
||
- **foreign**, if it isn't defined in the current crate | ||
- **local**, if it is defined in the current crate | ||
|
||
The distinction has significant implications for | ||
[coherence and orphan rules][1], as we'll get a chance to explore in this | ||
section of the course. | ||
|
||
- Compile the example to show the compiler error that's emitted. | ||
gribozavr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Highlight how the compiler error message nudges you towards the extension | ||
trait pattern. | ||
|
||
- Explain how many type-system restrictions in Rust aim to prevent _ambiguity_. | ||
gribozavr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
What would happen if you were allowed to define new inherent methods on | ||
foreign types? Different crates in your dependency tree might end up defining | ||
different methods on the same foreign type with the same name. | ||
|
||
As soon as there is room for ambiguity, there must be a way to disambiguate. | ||
If disambiguation happens implicitly, it can lead to surprising or otherwise | ||
unexpected behavior. If disambiguation happens explicitly, it can increase the | ||
cognitive load on developers who are reading your code. | ||
|
||
Furthermore, every time a crate defines a new inherent method on a foreign | ||
type, it may cause compilation errors in _your_ code, as you may be forced to | ||
introduce explicit disambiguation. | ||
|
||
Rust has decided to avoid the issue altogether by forbidding the definition of | ||
new inherent methods on foreign types. | ||
tall-vase marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- Other languages (e.g, Kotlin, C#, Swift) allow adding methods to existing | ||
types, often called "extension methods." This leads to different trade-offs in | ||
terms of potential ambiguities and the need for global reasoning. | ||
|
||
</details> | ||
|
||
[1]: https://doc.rust-lang.org/stable/reference/items/implementations.html#r-items.impl.trait.orphan-rule |
66 changes: 66 additions & 0 deletions
66
...diomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
--- | ||
minutes: 10 | ||
--- | ||
|
||
# Extending Foreign Types | ||
|
||
An **extension trait** is a local trait definition whose primary purpose is to | ||
attach new methods to foreign types. | ||
|
||
```rust | ||
mod ext { | ||
pub trait StrExt { | ||
fn is_palindrome(&self) -> bool; | ||
} | ||
|
||
impl StrExt for &str { | ||
fn is_palindrome(&self) -> bool { | ||
self.chars().eq(self.chars().rev()) | ||
} | ||
} | ||
} | ||
|
||
// Bring the extension trait into scope... | ||
pub use ext::StrExt as _; | ||
// ...then invoke its methods as if they were inherent methods | ||
assert!("dad".is_palindrome()); | ||
assert!(!"grandma".is_palindrome()); | ||
``` | ||
|
||
<details> | ||
|
||
- The `Ext` suffix is conventionally attached to the name of extension traits. | ||
|
||
It communicates that the trait is primarily used for extension purposes, and | ||
it is therefore not intended to be implemented outside the crate that defines | ||
it. | ||
|
||
Refer to the ["Extension Trait" RFC][1] as the authoritative source for naming | ||
conventions. | ||
|
||
- The extension trait implementation for a foreign type must be in the same | ||
crate as the trait itself, otherwise you'll be blocked by Rust's | ||
[_orphan rule_][2]. | ||
|
||
- The extension trait must be in scope when its methods are invoked. | ||
|
||
Comment out the `use` statement in the example to show the compiler error | ||
that's emitted if you try to invoke an extension method without having the | ||
corresponding extension trait in scope. | ||
|
||
- The example above uses an [_underscore import_][3] (`use ext::StringExt as _`) | ||
to minimize the likelihood of a naming conflict with other imported traits. | ||
|
||
With an underscore import, the trait is considered to be in scope and you're | ||
allowed to invoke its methods on types that implement the trait. Its _symbol_, | ||
instead, is not directly accessible. This prevents you, for example, from | ||
using that trait in a `where` clause. | ||
|
||
Since extension traits aren't meant to be used in `where` clauses, they are | ||
conventionally imported via an underscore import. | ||
|
||
</details> | ||
|
||
[1]: https://rust-lang.github.io/rfcs/0445-extension-trait-conventions.html | ||
[2]: https://github.com/rust-lang/rfcs/blob/master/text/2451-re-rebalancing-coherence.md#what-is-coherence-and-why-do-we-care | ||
[3]: https://doc.rust-lang.org/stable/reference/items/use-declarations.html#r-items.use.as-underscore |
102 changes: 102 additions & 0 deletions
102
...idiomatic/leveraging-the-type-system/extension-traits/extending-other-traits.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
--- | ||
minutes: 15 | ||
--- | ||
|
||
# Extending Other Traits | ||
randomPoison marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
As with types, it may be desirable to **extend foreign traits**. In particular, | ||
to attach new methods to _all_ implementors of a given trait. | ||
|
||
```rust | ||
mod ext { | ||
use std::fmt::Display; | ||
|
||
pub trait DisplayExt { | ||
fn quoted(&self) -> String; | ||
} | ||
|
||
impl<T: Display> DisplayExt for T { | ||
fn quoted(&self) -> String { | ||
format!("'{}'", self) | ||
} | ||
} | ||
} | ||
|
||
pub use ext::DisplayExt as _; | ||
|
||
assert_eq!("dad".quoted(), "'dad'"); | ||
assert_eq!(4.quoted(), "'4'"); | ||
assert_eq!(true.quoted(), "'true'"); | ||
LukeMathWalker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
<details> | ||
|
||
- Highlight how we added new behavior to _multiple_ types at once. `.quoted()` | ||
can be called on string slices, numbers, and booleans since they all implement | ||
the `Display` trait. | ||
|
||
This flavor of the extension trait pattern uses | ||
[_blanket implementations_][1]. | ||
|
||
A blanket implementation implements a trait for all types `T` that satisfy the | ||
trait bounds specified in the `impl` block. In this case, the only requirement | ||
is that `T` implements the `Display` trait. | ||
|
||
- Draw the students' attention to the implementation of `DisplayExt::quoted`: we | ||
can't make any assumptions about `T` other than that it implements `Display`. | ||
All our logic must either use methods from `Display` or functions/macros that | ||
don't require other traits. | ||
|
||
For example, we can call `format!` with `T`, but can't call `.to_uppercase()` | ||
because it is not necessarily a `String`. | ||
|
||
tall-vase marked this conversation as resolved.
Show resolved
Hide resolved
|
||
We could introduce additional trait bounds on `T`, but it would restrict the | ||
set of types that can leverage the extension trait. | ||
|
||
- Conventionally, the extension trait is named after the trait it extends, | ||
followed by the `Ext` suffix. In the example above, `DisplayExt`. | ||
|
||
- There are entire crates that extend standard library traits with new | ||
functionality. | ||
|
||
- `itertools` crate provides the `Itertools` trait that extends `Iterator`. It | ||
adds many iterator adapters, such as `interleave` and `unique`. It provides | ||
new algorithmic building blocks for iterator pipelines built with method | ||
chaining. | ||
|
||
- `futures` crate provides the `FutureExt` trait, which extends the `Future` | ||
trait with new combinators and helper methods. | ||
|
||
## More To Explore | ||
|
||
- Extension traits can be used by libraries to distinguish between stable and | ||
experimental methods. | ||
|
||
Stable methods are part of the trait definition. | ||
|
||
Experimental methods are provided via an extension trait defined in a | ||
different library, with a less restrictive stability policy. Some utility | ||
methods are then "promoted" to the core trait definition once they have been | ||
proven useful and their design has been refined. | ||
|
||
- Extension traits can be used to split a [dyn-incompatible trait][2] in two: | ||
tall-vase marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- A **dyn-compatible core**, restricted to the methods that satisfy | ||
dyn-compatibility requirements. | ||
- An **extension trait**, containing the remaining methods that are not | ||
dyn-compatible (e.g., methods with a generic parameter). | ||
|
||
- Concrete types that implement the core trait will be able to invoke all | ||
methods, thanks to the blanket impl for the extension trait. Trait objects | ||
(`dyn CoreTrait`) will be able to invoke all methods on the core trait as well | ||
as those on the extension trait that don't require `Self: Sized`. | ||
|
||
</details> | ||
|
||
[1]: https://doc.rust-lang.org/stable/reference/glossary.html#blanket-implementation | ||
[`itertools`]: https://docs.rs/itertools/latest/itertools/ | ||
[`Itertools`]: https://docs.rs/itertools/latest/itertools/trait.Itertools.html | ||
[`futures`]: https://docs.rs/futures/latest/futures/ | ||
[`FutureExt`]: https://docs.rs/futures/latest/futures/future/trait.FutureExt.html | ||
[`Future`]: https://docs.rs/futures/latest/futures/future/trait.Future.html | ||
[2]: https://doc.rust-lang.org/reference/items/traits.html#r-items.traits.dyn-compatible |
96 changes: 96 additions & 0 deletions
96
...atic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
--- | ||
minutes: 15 | ||
--- | ||
|
||
# Method Resolution Conflicts | ||
|
||
What happens when you have a name conflict between an inherent method and an | ||
extension method? | ||
gribozavr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```rust,editable | ||
mod ext { | ||
pub trait CountOnesExt { | ||
fn count_ones(&self) -> u32; | ||
} | ||
|
||
impl CountOnesExt for i32 { | ||
fn count_ones(&self) -> u32 { | ||
let value = *self; | ||
(0..32).filter(|i| ((value >> i) & 1i32) == 1).count() as u32 | ||
} | ||
} | ||
} | ||
fn main() { | ||
pub use ext::CountOnesExt; | ||
// Which `count_ones` method is invoked? | ||
// The one from `CountOnesExt`? Or the inherent one from `i32`? | ||
assert_eq!((-1i32).count_ones(), 32); | ||
} | ||
``` | ||
|
||
<details> | ||
|
||
- A foreign type may, in a newer version, add a new inherent method with the | ||
same name as our extension method. | ||
|
||
Ask: What will happen in the example above? Will there be a compiler error? | ||
Will one of the two methods be given higher priority? Which one? | ||
|
||
Add a `panic!("Extension trait");` in the body of `CountOnesExt::count_ones` | ||
to clarify which method is being invoked. | ||
|
||
- To prevent users of the Rust language from having to manually specify which | ||
method to use in all cases, there is a priority ordering system for how | ||
methods get "picked" first: | ||
- Immutable (`&self`) first | ||
- Inherent (method defined in the type's `impl` block) before Trait (method | ||
added by a trait impl). | ||
- Mutable (`&mut self`) Second | ||
- Inherent before Trait. | ||
|
||
If every method with the same name has different mutability and was either | ||
defined in as an inherent method or trait method, with no overlap, this makes | ||
the job easy for the compiler. | ||
|
||
This does introduce some ambiguity for the user, who may be confused as to why | ||
a method they're relying on is not producing expected behavior. Avoid name | ||
conflicts instead of relying on this mechanism if you can. | ||
|
||
Demonstrate: Change the signature and implementation of | ||
`CountOnesExt::count_ones` to `fn count_ones(&mut self) -> u32` and modify the | ||
invocation accordingly: | ||
|
||
```rust | ||
assert_eq!((&mut -1i32).count_ones(), 32); | ||
``` | ||
|
||
`CountOnesExt::count_ones` is invoked, rather than the inherent method, since | ||
`&mut self` has a higher priority than `&self`, the one used by the inherent | ||
method. | ||
|
||
If an immutable inherent method and a mutable trait method exist for the same | ||
type, we can specify which one to use at the call site by using | ||
`(&<value>).count_ones()` to get the immutable (higher priority) method or | ||
`(&mut <value>).count_ones()` | ||
|
||
Point the students to the Rust reference for more information on | ||
[method resolution][2]. | ||
|
||
- Avoid naming conflicts between extension trait methods and inherent methods. | ||
Rust's method resolution algorithm is complex and may surprise users of your | ||
code. | ||
|
||
## More to explore | ||
|
||
- The interaction between the priority search used by Rust's method resolution | ||
algorithm and automatic `Deref`ing can be used to emulate [specialization][4] | ||
on the stable toolchain, primarily in the context of macro-generated code. | ||
Check out ["Autoref Specialization"][5] for the specific details. | ||
|
||
</details> | ||
|
||
[1]: https://doc.rust-lang.org/stable/reference/expressions/method-call-expr.html#r-expr.method.candidate-search | ||
[2]: https://doc.rust-lang.org/stable/reference/expressions/method-call-expr.html | ||
[3]: https://github.com/rust-lang/reference/pull/1725 | ||
[4]: https://github.com/rust-lang/rust/issues/31844 | ||
[5]: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.