1
0
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:
Luca Palmieri
2025-10-20 12:45:06 +02:00
committed by GitHub
parent 9b091ee649
commit 72cdf8660d
7 changed files with 465 additions and 0 deletions

View File

@@ -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)

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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