Skip to content

Commit 72cdf86

Browse files
LukeMathWalkergribozavrtall-vaserandomPoisontall-vase
authored
Extension traits (#2812)
Co-authored-by: Dmitri Gribenko <[email protected]> Co-authored-by: tall-vase <[email protected]> Co-authored-by: Nicole L <[email protected]> Co-authored-by: tall-vase <[email protected]>
1 parent 9b091ee commit 72cdf86

File tree

7 files changed

+465
-0
lines changed

7 files changed

+465
-0
lines changed

src/SUMMARY.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,12 @@
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+
- [Trait Method Conflicts](idiomatic/leveraging-the-type-system/extension-traits/trait-method-conflicts.md)
444+
- [Extending Other Traits](idiomatic/leveraging-the-type-system/extension-traits/extending-other-traits.md)
445+
- [Should I Define An Extension Trait?](idiomatic/leveraging-the-type-system/extension-traits/should-i-define-an-extension-trait.md)
440446
- [Typestate Pattern](idiomatic/leveraging-the-type-system/typestate-pattern.md)
441447
- [Typestate Pattern Example](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-example.md)
442448
- [Beyond Simple Typestate](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
minutes: 15
3+
---
4+
5+
# Extension Traits
6+
7+
It may desirable to **extend** foreign types with new inherent methods. For
8+
example, allow your code to check if a string is a palindrome using
9+
method-calling syntax: `s.is_palindrome()`.
10+
11+
It might feel natural to reach out for an `impl` block:
12+
13+
```rust,compile_fail
14+
// 🛠️❌
15+
impl &'_ str {
16+
pub fn is_palindrome(&self) -> bool {
17+
self.chars().eq(self.chars().rev())
18+
}
19+
}
20+
```
21+
22+
The Rust compiler won't allow it, though. But you can use the **extension trait
23+
pattern** to work around this limitation.
24+
25+
<details>
26+
27+
- A Rust item (be it a trait or a type) is referred to as:
28+
29+
- **foreign**, if it isn't defined in the current crate
30+
- **local**, if it is defined in the current crate
31+
32+
The distinction has significant implications for
33+
[coherence and orphan rules][1], as we'll get a chance to explore in this
34+
section of the course.
35+
36+
- Compile the example to show the compiler error that's emitted.
37+
38+
Highlight how the compiler error message nudges you towards the extension
39+
trait pattern.
40+
41+
- Explain how many type-system restrictions in Rust aim to prevent _ambiguity_.
42+
43+
What would happen if you were allowed to define new inherent methods on
44+
foreign types? Different crates in your dependency tree might end up defining
45+
different methods on the same foreign type with the same name.
46+
47+
As soon as there is room for ambiguity, there must be a way to disambiguate.
48+
If disambiguation happens implicitly, it can lead to surprising or otherwise
49+
unexpected behavior. If disambiguation happens explicitly, it can increase the
50+
cognitive load on developers who are reading your code.
51+
52+
Furthermore, every time a crate defines a new inherent method on a foreign
53+
type, it may cause compilation errors in _your_ code, as you may be forced to
54+
introduce explicit disambiguation.
55+
56+
Rust has decided to avoid the issue altogether by forbidding the definition of
57+
new inherent methods on foreign types.
58+
59+
- Other languages (e.g, Kotlin, C#, Swift) allow adding methods to existing
60+
types, often called "extension methods." This leads to different trade-offs in
61+
terms of potential ambiguities and the need for global reasoning.
62+
63+
</details>
64+
65+
[1]: https://doc.rust-lang.org/stable/reference/items/implementations.html#r-items.impl.trait.orphan-rule
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
minutes: 10
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 extension trait implementation for a foreign type must be in the same
42+
crate as the trait itself, 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 example above uses an [_underscore import_][3] (`use ext::StringExt as _`)
52+
to minimize the likelihood of a naming conflict with other imported traits.
53+
54+
With an underscore import, the trait is considered to be in scope and you're
55+
allowed to invoke its methods on types that implement the trait. Its _symbol_,
56+
instead, is not directly accessible. This prevents you, for example, from
57+
using that trait in a `where` clause.
58+
59+
Since extension traits aren't meant to be used in `where` clauses, they are
60+
conventionally imported via an underscore import.
61+
62+
</details>
63+
64+
[1]: https://rust-lang.github.io/rfcs/0445-extension-trait-conventions.html
65+
[2]: https://github.com/rust-lang/rfcs/blob/master/text/2451-re-rebalancing-coherence.md#what-is-coherence-and-why-do-we-care
66+
[3]: https://doc.rust-lang.org/stable/reference/items/use-declarations.html#r-items.use.as-underscore
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
minutes: 15
3+
---
4+
5+
# Extending Other Traits
6+
7+
As with types, it may be desirable to **extend foreign traits**. In particular,
8+
to attach new methods to _all_ implementors of a given trait.
9+
10+
```rust
11+
mod ext {
12+
use std::fmt::Display;
13+
14+
pub trait DisplayExt {
15+
fn quoted(&self) -> String;
16+
}
17+
18+
impl<T: Display> DisplayExt for T {
19+
fn quoted(&self) -> String {
20+
format!("'{}'", self)
21+
}
22+
}
23+
}
24+
25+
pub use ext::DisplayExt as _;
26+
27+
assert_eq!("dad".quoted(), "'dad'");
28+
assert_eq!(4.quoted(), "'4'");
29+
assert_eq!(true.quoted(), "'true'");
30+
```
31+
32+
<details>
33+
34+
- Highlight how we added new behavior to _multiple_ types at once. `.quoted()`
35+
can be called on string slices, numbers, and booleans since they all implement
36+
the `Display` trait.
37+
38+
This flavor of the extension trait pattern uses
39+
[_blanket implementations_][1].
40+
41+
A blanket implementation implements a trait for all types `T` that satisfy the
42+
trait bounds specified in the `impl` block. In this case, the only requirement
43+
is that `T` implements the `Display` trait.
44+
45+
- Draw the students' attention to the implementation of `DisplayExt::quoted`: we
46+
can't make any assumptions about `T` other than that it implements `Display`.
47+
All our logic must either use methods from `Display` or functions/macros that
48+
don't require other traits.
49+
50+
For example, we can call `format!` with `T`, but can't call `.to_uppercase()`
51+
because it is not necessarily a `String`.
52+
53+
We could introduce additional trait bounds on `T`, but it would restrict the
54+
set of types that can leverage the extension trait.
55+
56+
- Conventionally, the extension trait is named after the trait it extends,
57+
followed by the `Ext` suffix. In the example above, `DisplayExt`.
58+
59+
- There are entire crates that extend standard library traits with new
60+
functionality.
61+
62+
- `itertools` crate provides the `Itertools` trait that extends `Iterator`. It
63+
adds many iterator adapters, such as `interleave` and `unique`. It provides
64+
new algorithmic building blocks for iterator pipelines built with method
65+
chaining.
66+
67+
- `futures` crate provides the `FutureExt` trait, which extends the `Future`
68+
trait with new combinators and helper methods.
69+
70+
## More To Explore
71+
72+
- Extension traits can be used by libraries to distinguish between stable and
73+
experimental methods.
74+
75+
Stable methods are part of the trait definition.
76+
77+
Experimental methods are provided via an extension trait defined in a
78+
different library, with a less restrictive stability policy. Some utility
79+
methods are then "promoted" to the core trait definition once they have been
80+
proven useful and their design has been refined.
81+
82+
- Extension traits can be used to split a [dyn-incompatible trait][2] in two:
83+
84+
- A **dyn-compatible core**, restricted to the methods that satisfy
85+
dyn-compatibility requirements.
86+
- An **extension trait**, containing the remaining methods that are not
87+
dyn-compatible (e.g., methods with a generic parameter).
88+
89+
- Concrete types that implement the core trait will be able to invoke all
90+
methods, thanks to the blanket impl for the extension trait. Trait objects
91+
(`dyn CoreTrait`) will be able to invoke all methods on the core trait as well
92+
as those on the extension trait that don't require `Self: Sized`.
93+
94+
</details>
95+
96+
[1]: https://doc.rust-lang.org/stable/reference/glossary.html#blanket-implementation
97+
[`itertools`]: https://docs.rs/itertools/latest/itertools/
98+
[`Itertools`]: https://docs.rs/itertools/latest/itertools/trait.Itertools.html
99+
[`futures`]: https://docs.rs/futures/latest/futures/
100+
[`FutureExt`]: https://docs.rs/futures/latest/futures/future/trait.FutureExt.html
101+
[`Future`]: https://docs.rs/futures/latest/futures/future/trait.Future.html
102+
[2]: https://doc.rust-lang.org/reference/items/traits.html#r-items.traits.dyn-compatible
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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,editable
11+
mod ext {
12+
pub trait CountOnesExt {
13+
fn count_ones(&self) -> u32;
14+
}
15+
16+
impl CountOnesExt for i32 {
17+
fn count_ones(&self) -> u32 {
18+
let value = *self;
19+
(0..32).filter(|i| ((value >> i) & 1i32) == 1).count() as u32
20+
}
21+
}
22+
}
23+
fn main() {
24+
pub use ext::CountOnesExt;
25+
// Which `count_ones` method is invoked?
26+
// The one from `CountOnesExt`? Or the inherent one from `i32`?
27+
assert_eq!((-1i32).count_ones(), 32);
28+
}
29+
```
30+
31+
<details>
32+
33+
- A foreign type may, in a newer version, add a new inherent method with the
34+
same name as our extension method.
35+
36+
Ask: What will happen in the example above? Will there be a compiler error?
37+
Will one of the two methods be given higher priority? Which one?
38+
39+
Add a `panic!("Extension trait");` in the body of `CountOnesExt::count_ones`
40+
to clarify which method is being invoked.
41+
42+
- To prevent users of the Rust language from having to manually specify which
43+
method to use in all cases, there is a priority ordering system for how
44+
methods get "picked" first:
45+
- Immutable (`&self`) first
46+
- Inherent (method defined in the type's `impl` block) before Trait (method
47+
added by a trait impl).
48+
- Mutable (`&mut self`) Second
49+
- Inherent before Trait.
50+
51+
If every method with the same name has different mutability and was either
52+
defined in as an inherent method or trait method, with no overlap, this makes
53+
the job easy for the compiler.
54+
55+
This does introduce some ambiguity for the user, who may be confused as to why
56+
a method they're relying on is not producing expected behavior. Avoid name
57+
conflicts instead of relying on this mechanism if you can.
58+
59+
Demonstrate: Change the signature and implementation of
60+
`CountOnesExt::count_ones` to `fn count_ones(&mut self) -> u32` and modify the
61+
invocation accordingly:
62+
63+
```rust
64+
assert_eq!((&mut -1i32).count_ones(), 32);
65+
```
66+
67+
`CountOnesExt::count_ones` is invoked, rather than the inherent method, since
68+
`&mut self` has a higher priority than `&self`, the one used by the inherent
69+
method.
70+
71+
If an immutable inherent method and a mutable trait method exist for the same
72+
type, we can specify which one to use at the call site by using
73+
`(&<value>).count_ones()` to get the immutable (higher priority) method or
74+
`(&mut <value>).count_ones()`
75+
76+
Point the students to the Rust reference for more information on
77+
[method resolution][2].
78+
79+
- Avoid naming conflicts between extension trait methods and inherent methods.
80+
Rust's method resolution algorithm is complex and may surprise users of your
81+
code.
82+
83+
## More to explore
84+
85+
- The interaction between the priority search used by Rust's method resolution
86+
algorithm and automatic `Deref`ing can be used to emulate [specialization][4]
87+
on the stable toolchain, primarily in the context of macro-generated code.
88+
Check out ["Autoref Specialization"][5] for the specific details.
89+
90+
</details>
91+
92+
[1]: https://doc.rust-lang.org/stable/reference/expressions/method-call-expr.html#r-expr.method.candidate-search
93+
[2]: https://doc.rust-lang.org/stable/reference/expressions/method-call-expr.html
94+
[3]: https://github.com/rust-lang/reference/pull/1725
95+
[4]: https://github.com/rust-lang/rust/issues/31844
96+
[5]: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md

0 commit comments

Comments
 (0)