Skip to content

Commit be796c3

Browse files
Extension traits
1 parent 4483602 commit be796c3

File tree

5 files changed

+207
-0
lines changed

5 files changed

+207
-0
lines changed

src/SUMMARY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,10 @@
437437
- [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md)
438438
- [Parse, Don't Validate](idiomatic/leveraging-the-type-system/newtype-pattern/parse-don-t-validate.md)
439439
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
440+
- [Extension Traits](idiomatic/leveraging-the-type-system/extension-traits.md)
441+
- [Extending Foreign Types](idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md)
442+
- [Method Resolution Conflicts](idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md)
443+
- [Extending Foreign Traits](idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-traits.md)
440444

441445
---
442446

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
minutes: 5
3+
---
4+
5+
# Extension Traits
6+
7+
In Rust, you can't define new inherent methods for foreign types.
8+
9+
```rust,compile_fail
10+
// 🛠️❌
11+
impl &'_ str {
12+
pub fn is_palindrome(&self) -> bool {
13+
self.chars().eq(self.chars().rev())
14+
}
15+
}
16+
```
17+
18+
You can use the **extension trait pattern** to work around this limitation.
19+
20+
<details>
21+
22+
- Try to compile the example to show the compiler error that's emitted.
23+
24+
Point out, in particular, how the compiler error message nudges you towards
25+
the extension trait pattern.
26+
27+
- Explain how many type-system restrictions in Rust aim to prevent _ambiguity_.
28+
29+
If you were allowed to define new inherent methods on foreign types, there
30+
would need to be a mechanism to disambiguate between distinct inherent methods
31+
with the same name.
32+
33+
In particular, adding a new inherent method to a library type could cause
34+
errors in downstream code if the name of the new method conflicts with an
35+
inherent method that's been defined in the consuming crate.
36+
37+
</details>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Extending Foreign Traits
2+
3+
- TODO: Show how extension traits can be used to extend traits rather than
4+
types.
5+
- TODO: Show disambiguation syntax for naming conflicts between trait methods
6+
and extension trait methods.
7+
- https://github.com/rust-lang/rfcs/blob/master/text/0132-ufcs.md
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
minutes: 15
3+
---
4+
5+
# Extending Foreign Types
6+
7+
An **extension trait** is a local trait definition whose primary purpose is to
8+
attach new methods to foreign types.
9+
10+
```rust
11+
mod ext {
12+
pub trait StrExt {
13+
fn is_palindrome(&self) -> bool;
14+
}
15+
16+
impl StrExt for &str {
17+
fn is_palindrome(&self) -> bool {
18+
self.chars().eq(self.chars().rev())
19+
}
20+
}
21+
}
22+
23+
// Bring the extension trait into scope..
24+
pub use ext::StrExt as _;
25+
// ..then invoke its methods as if they were inherent methods
26+
assert!("dad".is_palindrome());
27+
assert!(!"grandma".is_palindrome());
28+
```
29+
30+
<details>
31+
32+
- The `Ext` suffix is conventionally attached to the name of extension traits.
33+
34+
It communicates that the trait is primarily used for extension purposes, and
35+
it is therefore not intended to be implemented outside the crate that defines
36+
it.
37+
38+
Refer to the ["Extension Trait" RFC][1] as the authoritative source for naming
39+
conventions.
40+
41+
- The trait implementation for the chosen foreign type must belong to the same
42+
crate where the trait is defined, otherwise you'll be blocked by Rust's
43+
[_orphan rule_][2].
44+
45+
- The extension trait must be in scope when its methods are invoked.
46+
47+
Comment out the `use` statement in the example to show the compiler error
48+
that's emitted if you try to invoke an extension method without having the
49+
corresponding extension trait in scope.
50+
51+
- The `as _` syntax reduces the likelihood of naming conflicts when multiple
52+
traits are imported. It is conventionally used when importing extension
53+
traits.
54+
55+
- Some students may be wondering: does the extension trait pattern provide
56+
enough value to justify the additional boilerplate? Wouldn't a free function
57+
be enough?
58+
59+
Show how the same example could be implemented using an `is_palindrome` free
60+
function, with a single `&str` input parameter:
61+
62+
```rust
63+
fn is_palindrome(s: &str) -> bool {
64+
s.chars().eq(s.chars().rev())
65+
}
66+
```
67+
68+
A bespoke extension trait might be an overkill if you want to add a single
69+
method to a foreign type. Both a free function and an extension trait will
70+
require an additional import, and the familiarity of the method calling syntax
71+
may not be enough to justify the boilerplate of a trait definition.
72+
73+
Nonetheless, extension methods can be **easier to discover** than free
74+
functions. In particular, language servers (e.g. `rust-analyzer`) will suggest
75+
extension methods if you type `.` after an instance of the foreign type.
76+
77+
</details>
78+
79+
[1]: https://rust-lang.github.io/rfcs/0445-extension-trait-rfc.html
80+
[2]: https://github.com/rust-lang/rfcs/blob/master/text/2451-re-rebalancing-coherence.md#what-is-coherence-and-why-do-we-care
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
minutes: 15
3+
---
4+
5+
# Method Resolution Conflicts
6+
7+
What happens when you have a name conflict between an inherent method and an
8+
extension method?
9+
10+
```rust
11+
mod ext {
12+
pub trait StrExt {
13+
fn trim_ascii(&self) -> &str;
14+
}
15+
16+
impl StrExt for &str {
17+
fn trim_ascii(&self) -> &str {
18+
self.trim_start_matches(|c: char| c.is_ascii_whitespace())
19+
}
20+
}
21+
}
22+
23+
pub use ext::StrExt;
24+
// Which `trim_ascii` method is invoked?
25+
// The one from `StrExt`? Or the inherent one from `str`?
26+
assert_eq!(" dad ".trim_ascii(), "dad");
27+
```
28+
29+
<details>
30+
31+
- The foreign type may, in a newer version, add a new inherent method with the
32+
same name of our extension method.
33+
34+
Survey the class: what do the students think will happen in the example above?
35+
Will there be a compiler error? Will one of the two methods be given higher
36+
priority? Which one?
37+
38+
Add a `panic!("Extension trait")` in the body of `StrExt::trim_ascii` to
39+
clarify which method is being invoked.
40+
41+
- [Inherent methods have higher priority than trait methods][1], _if_ they have
42+
the same name and the **same receiver**, e.g. they both expect `&self` as
43+
input. The situation becomes more nuanced if the use a **different receiver**,
44+
e.g. `&mut self` vs `&self`.
45+
46+
Change the signature of `StrExt::trim_ascii` to
47+
`fn trim_ascii(&mut self) -> &str` and modify the invocation accordingly:
48+
49+
```rust
50+
assert_eq!((&mut " dad ").trim_ascii(), "dad");
51+
```
52+
53+
Now `StrExt::trim_ascii` is invoked, rather than the inherent method, since
54+
`&mut self` is a more specific receiver than `&self`, the one used by the
55+
inherent method.
56+
57+
Point the students to the Rust reference for more information on
58+
[method resolution][2]. An explanation with more extensive examples can be
59+
found in [an open PR to the Rust reference][3].
60+
61+
- Avoid naming conflicts between extension trait methods and inherent methods.
62+
Rust's method resolution algorithm is complex and may surprise users of your
63+
code.
64+
65+
## More to explore
66+
67+
- The interaction between the priority search used by Rust's method resolution
68+
algorithm and automatic `Deref`ering can be used to emulate
69+
[specialization][4] on the stable toolchain, primarily in the context of
70+
macro-generated code. Check out ["Autoref Specialization"][5] for the specific
71+
details.
72+
73+
</details>
74+
75+
[1]: https://doc.rust-lang.org/stable/reference/expressions/method-call-expr.html#r-expr.method.candidate-search
76+
[2]: https://doc.rust-lang.org/stable/reference/expressions/method-call-expr.html
77+
[3]: https://github.com/rust-lang/reference/pull/1725
78+
[4]: https://github.com/rust-lang/rust/issues/31844
79+
[5]: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md

0 commit comments

Comments
 (0)