-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
base: main
Are you sure you want to change the base?
Extension traits #2812
Changes from 2 commits
be796c3
408962c
1670b64
bd1b26d
06f251e
21a105e
b64886c
c36f7fe
5ab37d8
841bce5
aa2ab0f
63d3aa3
17ba065
d10c985
771a37a
b58a2a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
--- | ||
minutes: 5 | ||
--- | ||
|
||
# Extension Traits | ||
|
||
In Rust, you can't define new inherent methods for foreign types. | ||
|
||
```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()) | ||
} | ||
} | ||
``` | ||
|
||
You can use the **extension trait pattern** to work around this limitation. | ||
|
||
<details> | ||
|
||
- Compile the example to show the compiler error that's emitted. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should start by explaining what we want to achieve first: we want the user to be able to write something like Otherwise it might be confusing to some audience members: we are starting a new chapter by looking at a piece of code that does not compile (the impl block above), we want it to compile (why?..), but we actually wouldn't, instead we should do something else. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I restructured the flow a bit in 17ba065. What do you think? |
||
|
||
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_. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can raise the level of abstraction even higher. One effective way to approach evaluating features in programming design is to ask "what if everybody did this?" "I want to be able to add methods to someone else's type! I want to add an is_palindrome method to a string" - "Yes, but what if two people did this?" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 17ba065 |
||
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once you've said that "there would need to be a mechanism to disambiguate", the objection in this paragraph is rather weak. Yes, the methods could conflict, but we've already resigned to adding a disambiguation rule. So whenever we identify a case where methods conflict, we already know a fix - add a disambiguation rule, what's the big deal about it. I think a better way to approach this explanation with fewer hypotheticals is to say that Rust doesn't even want to be in the business of designing these disambiguation rules, which might need to be quite complex in order to be effective at disambiguating. (You could draw a parallel with overload resolution in C++, which is extremely complex and subtle.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tried in 17ba065. |
||
|
||
</details> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
LukeMathWalker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does it do though? (I know what it does, this is a request to improve the text.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have expanded on this syntax in 17ba065, with a link to the reference. |
||
traits are imported. It is conventionally used when importing extension | ||
traits. | ||
|
||
- Some students may be wondering: does the extension trait pattern provide | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be a separate slide: when to use an extension trait There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sub-slides can be good for this -- the main content is done, and there's a few more details to think about. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracted in b58a2a5 |
||
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 | ||
LukeMathWalker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[2]: https://github.com/rust-lang/rfcs/blob/master/text/2451-re-rebalancing-coherence.md#what-is-coherence-and-why-do-we-care |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -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? | ||||||||||||
gribozavr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
||||||||||||
```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 | ||||||||||||
gribozavr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
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 | ||||||||||||
LukeMathWalker marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
input. The situation becomes more nuanced if the use a **different receiver**, | ||||||||||||
e.g. `&mut self` vs `&self`. | ||||||||||||
LukeMathWalker marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
||||||||||||
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 | ||||||||||||
LukeMathWalker marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
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]. | ||||||||||||
Comment on lines
+57
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think we can just link to the reference, I don't think linking to an open PR is necessary. Eventually the things in that PR will (hopefully) land, so just linking to the reference is enough imo. |
||||||||||||
|
||||||||||||
- 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 | ||||||||||||
LukeMathWalker marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
[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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is "foreign type" a standard term to refer to types from other crates? I feel like it can be easily misinterpreted as "types defined in C++" (similar to "foreign functions"). Is there an alternative?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Foreign type" and "foreign trait" are the terms used in the Rust reference when discussing orphan rules (see this section). So I'd say they are standard terms in this specific context.
Alternatively, we could use "new inherent methods for a type defined in another crate" as an alternative phrasing. It could get fairly verbose in the speaker notes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ack. Maybe then briefly mention in the speaker notes what these terms mean, hinting that the instructor should maybe explain them. I don't think it is a given that the audience understands coherence rules and understands why it matters where the trait is defined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in 17ba065