You've already forked comprehensive-rust
mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-07-14 18:14:29 +02:00
Extension traits
This commit is contained in:
@ -437,6 +437,10 @@
|
||||
- [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)
|
||||
- [Extending Foreign Traits](idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-traits.md)
|
||||
|
||||
---
|
||||
|
||||
|
37
src/idiomatic/leveraging-the-type-system/extension-traits.md
Normal file
37
src/idiomatic/leveraging-the-type-system/extension-traits.md
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
minutes: 5
|
||||
---
|
||||
|
||||
# Extension Traits
|
||||
|
||||
In Rust, you can't define new inherent methods for foreign types.
|
||||
|
||||
```rust,compile_fail
|
||||
// 🛠️❌
|
||||
impl &'_ str {
|
||||
pub fn is_palindrome(&self) -> bool {
|
||||
self.chars().eq(self.chars().rev())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can use the **extension trait pattern** to work around this limitation.
|
||||
|
||||
<details>
|
||||
|
||||
- Try to compile the example to show the compiler error that's emitted.
|
||||
|
||||
Point out, in particular, how the compiler error message nudges you towards
|
||||
the extension trait pattern.
|
||||
|
||||
- Explain how many type-system restrictions in Rust aim to prevent _ambiguity_.
|
||||
|
||||
If you were allowed to define new inherent methods on foreign types, there
|
||||
would need to be a mechanism to disambiguate between distinct inherent methods
|
||||
with the same name.
|
||||
|
||||
In particular, adding a new inherent method to a library type could cause
|
||||
errors in downstream code if the name of the new method conflicts with an
|
||||
inherent method that's been defined in the consuming crate.
|
||||
|
||||
</details>
|
@ -0,0 +1,7 @@
|
||||
# Extending Foreign Traits
|
||||
|
||||
- TODO: Show how extension traits can be used to extend traits rather than
|
||||
types.
|
||||
- TODO: Show disambiguation syntax for naming conflicts between trait methods
|
||||
and extension trait methods.
|
||||
- https://github.com/rust-lang/rfcs/blob/master/text/0132-ufcs.md
|
@ -0,0 +1,80 @@
|
||||
---
|
||||
minutes: 15
|
||||
---
|
||||
|
||||
# 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 trait implementation for the chosen foreign type must belong to the same
|
||||
crate where the trait is defined, 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 `as _` syntax reduces the likelihood of naming conflicts when multiple
|
||||
traits are imported. It is conventionally used when importing extension
|
||||
traits.
|
||||
|
||||
- Some students may be wondering: does the extension trait pattern provide
|
||||
enough value to justify the additional boilerplate? Wouldn't a free function
|
||||
be enough?
|
||||
|
||||
Show how the same example could be implemented using an `is_palindrome` free
|
||||
function, with a single `&str` input parameter:
|
||||
|
||||
```rust
|
||||
fn is_palindrome(s: &str) -> bool {
|
||||
s.chars().eq(s.chars().rev())
|
||||
}
|
||||
```
|
||||
|
||||
A bespoke extension trait might be an overkill if you want to add a single
|
||||
method to a foreign type. Both a free function and an extension trait will
|
||||
require an additional import, and the familiarity of the method calling syntax
|
||||
may not be enough to justify the boilerplate of a trait definition.
|
||||
|
||||
Nonetheless, extension methods can be **easier to discover** than free
|
||||
functions. In particular, language servers (e.g. `rust-analyzer`) will suggest
|
||||
extension methods if you type `.` after an instance of the foreign type.
|
||||
|
||||
</details>
|
||||
|
||||
[1]: https://rust-lang.github.io/rfcs/0445-extension-trait-rfc.html
|
||||
[2]: https://github.com/rust-lang/rfcs/blob/master/text/2451-re-rebalancing-coherence.md#what-is-coherence-and-why-do-we-care
|
@ -0,0 +1,79 @@
|
||||
---
|
||||
minutes: 15
|
||||
---
|
||||
|
||||
# Method Resolution Conflicts
|
||||
|
||||
What happens when you have a name conflict between an inherent method and an
|
||||
extension method?
|
||||
|
||||
```rust
|
||||
mod ext {
|
||||
pub trait StrExt {
|
||||
fn trim_ascii(&self) -> &str;
|
||||
}
|
||||
|
||||
impl StrExt for &str {
|
||||
fn trim_ascii(&self) -> &str {
|
||||
self.trim_start_matches(|c: char| c.is_ascii_whitespace())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use ext::StrExt;
|
||||
// Which `trim_ascii` method is invoked?
|
||||
// The one from `StrExt`? Or the inherent one from `str`?
|
||||
assert_eq!(" dad ".trim_ascii(), "dad");
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- The foreign type may, in a newer version, add a new inherent method with the
|
||||
same name of our extension method.
|
||||
|
||||
Survey the class: what do the students think 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 `StrExt::trim_ascii` to
|
||||
clarify which method is being invoked.
|
||||
|
||||
- [Inherent methods have higher priority than trait methods][1], _if_ they have
|
||||
the same name and the **same receiver**, e.g. they both expect `&self` as
|
||||
input. The situation becomes more nuanced if the use a **different receiver**,
|
||||
e.g. `&mut self` vs `&self`.
|
||||
|
||||
Change the signature of `StrExt::trim_ascii` to
|
||||
`fn trim_ascii(&mut self) -> &str` and modify the invocation accordingly:
|
||||
|
||||
```rust
|
||||
assert_eq!((&mut " dad ").trim_ascii(), "dad");
|
||||
```
|
||||
|
||||
Now `StrExt::trim_ascii` is invoked, rather than the inherent method, since
|
||||
`&mut self` is a more specific receiver than `&self`, the one used by the
|
||||
inherent method.
|
||||
|
||||
Point the students to the Rust reference for more information on
|
||||
[method resolution][2]. An explanation with more extensive examples can be
|
||||
found in [an open PR to the Rust reference][3].
|
||||
|
||||
- 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`ering 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
|
Reference in New Issue
Block a user