You've already forked comprehensive-rust
mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-12-24 07:19:47 +02:00
Extension traits (#2812)
Co-authored-by: Dmitri Gribenko <gribozavr@gmail.com> Co-authored-by: tall-vase <228449146+tall-vase@users.noreply.github.com> Co-authored-by: Nicole L <dlegare.1001@gmail.com> Co-authored-by: tall-vase <fiona@mainmatter.com>
This commit is contained in:
@@ -437,6 +437,12 @@
|
||||
- [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md)
|
||||
- [Parse, Don't Validate](idiomatic/leveraging-the-type-system/newtype-pattern/parse-don-t-validate.md)
|
||||
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
|
||||
- [Extension Traits](idiomatic/leveraging-the-type-system/extension-traits.md)
|
||||
- [Extending Foreign Types](idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md)
|
||||
- [Method Resolution Conflicts](idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md)
|
||||
- [Trait Method Conflicts](idiomatic/leveraging-the-type-system/extension-traits/trait-method-conflicts.md)
|
||||
- [Extending Other Traits](idiomatic/leveraging-the-type-system/extension-traits/extending-other-traits.md)
|
||||
- [Should I Define An Extension Trait?](idiomatic/leveraging-the-type-system/extension-traits/should-i-define-an-extension-trait.md)
|
||||
- [Typestate Pattern](idiomatic/leveraging-the-type-system/typestate-pattern.md)
|
||||
- [Typestate Pattern Example](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-example.md)
|
||||
- [Beyond Simple Typestate](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md)
|
||||
|
||||
65
src/idiomatic/leveraging-the-type-system/extension-traits.md
Normal file
65
src/idiomatic/leveraging-the-type-system/extension-traits.md
Normal file
@@ -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
|
||||
// 🛠️❌
|
||||
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.
|
||||
|
||||
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_.
|
||||
|
||||
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.
|
||||
|
||||
- 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
|
||||
@@ -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
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
minutes: 15
|
||||
---
|
||||
|
||||
# Extending Other Traits
|
||||
|
||||
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'");
|
||||
```
|
||||
|
||||
<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`.
|
||||
|
||||
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:
|
||||
|
||||
- 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
|
||||
@@ -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?
|
||||
|
||||
```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
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
minutes: 5
|
||||
---
|
||||
|
||||
# Should I Define An Extension Trait?
|
||||
|
||||
In what scenarios should you prefer an extension trait over a free function?
|
||||
|
||||
```rust
|
||||
pub trait StrExt {
|
||||
fn is_palindrome(&self) -> bool;
|
||||
}
|
||||
|
||||
impl StrExt for &str {
|
||||
fn is_palindrome(&self) -> bool {
|
||||
self.chars().eq(self.chars().rev())
|
||||
}
|
||||
}
|
||||
|
||||
// vs
|
||||
|
||||
fn is_palindrome(s: &str) -> bool {
|
||||
s.chars().eq(s.chars().rev())
|
||||
}
|
||||
```
|
||||
|
||||
The main advantage of extension traits is **ease of discovery**.
|
||||
|
||||
<details>
|
||||
|
||||
- Extension methods can be easier to discover than free functions. Language
|
||||
servers (e.g., `rust-analyzer`) will suggest them if you type `.` after an
|
||||
instance of the foreign type.
|
||||
|
||||
- However, a bespoke extension trait might be overkill for a single method. Both
|
||||
approaches require an additional import, and the familiar method syntax may
|
||||
not justify the boilerplate of a full trait definition.
|
||||
|
||||
- **Discoverability:** Extension methods are easier to discover than free
|
||||
functions. Language servers (e.g., `rust-analyzer`) will suggest them if you
|
||||
type `.` after an instance of the foreign type.
|
||||
|
||||
- **Method Chaining:** A major ergonomic win for extension traits is method
|
||||
chaining. This is the foundation of the `Iterator` trait, allowing for fluent
|
||||
calls like `data.iter().filter(...).map(...)`. Achieving this with free
|
||||
functions would be far more cumbersome (`map(filter(iter(data), ...), ...)`).
|
||||
|
||||
- **API Cohesion:** Extension traits help create a cohesive API. If you have
|
||||
several related functions for a foreign type (e.g., `is_palindrome`,
|
||||
`word_count`, `to_kebab_case`), grouping them in a single `StrExt` trait is
|
||||
often cleaner than having multiple free functions for a user to import.
|
||||
|
||||
- **Trade-offs:** Despite these advantages, a bespoke extension trait might be
|
||||
overkill for a single, simple function. Both approaches require an additional
|
||||
import, and the familiar method syntax may not justify the boilerplate of a
|
||||
full trait definition.
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
minutes: 5
|
||||
---
|
||||
|
||||
# Trait Method Conflicts
|
||||
|
||||
What happens when you have a name conflict between two different trait methods
|
||||
implemented for the same type?
|
||||
|
||||
<!-- dprint -->
|
||||
|
||||
```rust,editable,compile_fail
|
||||
mod ext {
|
||||
pub trait Ext1 {
|
||||
fn is_palindrome(&self) -> bool;
|
||||
}
|
||||
|
||||
pub trait Ext2 {
|
||||
fn is_palindrome(&self) -> bool;
|
||||
}
|
||||
|
||||
impl Ext1 for &str {
|
||||
fn is_palindrome(&self) -> bool {
|
||||
self.chars().eq(self.chars().rev())
|
||||
}
|
||||
}
|
||||
|
||||
impl Ext2 for &str {
|
||||
fn is_palindrome(&self) -> bool {
|
||||
self.chars().eq(self.chars().rev())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use ext::{Ext1, Ext2};
|
||||
|
||||
// Which method is invoked?
|
||||
// The one from `Ext1`? Or the one from `Ext2`?
|
||||
fn main() {
|
||||
assert!("dad".is_palindrome());
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- The trait you are extending may, in a newer version, add a new trait method
|
||||
with the same name as your extension method. Or another extension trait for
|
||||
the same type may define a method with a name that conflicts with your own
|
||||
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?
|
||||
|
||||
- The compiler rejects the code because it cannot determine which method to
|
||||
invoke. Neither `Ext1` nor `Ext2` has a higher priority than the other.
|
||||
|
||||
To resolve this conflict, you must specify which trait you want to use.
|
||||
|
||||
Demonstrate: call `Ext1::is_palindrome(&"dad")` or
|
||||
`Ext2::is_palindrome(&"dad")` instead of `"dad".is_palindrome()`.
|
||||
|
||||
For methods with more complex signatures, you may need to use a more explicit
|
||||
[fully-qualified syntax][1].
|
||||
|
||||
- Demonstrate: replace `"dad".is_palindrome()` with
|
||||
`<&str as Ext1>::is_palindrome(&"dad")` or
|
||||
`<&str as
|
||||
Ext2>::is_palindrome(&"dad")`.
|
||||
|
||||
</details>
|
||||
|
||||
[1]: https://doc.rust-lang.org/reference/expressions/call-expr.html#disambiguating-function-calls
|
||||
Reference in New Issue
Block a user