diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 1950476a423a..6c22e06eff86 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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) + - [Should I Define An Extension Trait?](idiomatic/leveraging-the-type-system/extension-traits/should-i-define-an-extension-trait.md) + - [Extending Other Traits](idiomatic/leveraging-the-type-system/extension-traits/extending-other-traits.md) + - [Trait Method Conflicts](idiomatic/leveraging-the-type-system/extension-traits/trait-method-conflicts.md) --- diff --git a/src/idiomatic/leveraging-the-type-system/extension-traits.md b/src/idiomatic/leveraging-the-type-system/extension-traits.md new file mode 100644 index 000000000000..f5bc3025b189 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/extension-traits.md @@ -0,0 +1,63 @@ +--- +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. + +
+ +- Start by explaining the terminology. + + 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. + +
+ +[1]: https://doc.rust-lang.org/stable/reference/items/implementations.html#r-items.impl.trait.orphan-rule diff --git a/src/idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md b/src/idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md new file mode 100644 index 000000000000..bf781700f09a --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md @@ -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()); +``` + +
+ +- 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 example above uses an [_underscore import_][3] (`use ext::StrExt 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. + +
+ +[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 diff --git a/src/idiomatic/leveraging-the-type-system/extension-traits/extending-other-traits.md b/src/idiomatic/leveraging-the-type-system/extension-traits/extending-other-traits.md new file mode 100644 index 000000000000..d8d4e8ba34fd --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/extension-traits/extending-other-traits.md @@ -0,0 +1,95 @@ +--- +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 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'"); +``` + +
+ +- Highlight how we added new behaviour to _multiple_ distinct types at once. + `.quoted()` can be called on string slices, numbers and booleans since they + all implement the `Display` trait. + + This flavour of the extension trait pattern is built on top of + [_blanket implementations_][1]. + + Blanket implementations allow us to implement a trait for a generic type `T`, + as long as it satisfies 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 the type of `T` other than that it implements + `Display`. All our logic must either use methods from `Display` or + functions/macros that doesn't require `T` to implement any other trait. + + 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, + following by the `Ext` suffix. In the example above, `DisplayExt`. + +- There are entire libraries aimed at extending foundational traits with new + functionality. + + [`itertools`] provides a wide range of iterator adapters and utilities via the + [`Itertools`] trait. [`futures`] provides [`FutureExt`] to extend the + [`Future`] trait. + +## 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`. + +
+ +[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 diff --git a/src/idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md b/src/idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md new file mode 100644 index 000000000000..df91c0e58360 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md @@ -0,0 +1,78 @@ +--- +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"); +``` + +
+ +- 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` has a higher priority 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`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. + +
+ +[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 diff --git a/src/idiomatic/leveraging-the-type-system/extension-traits/should-i-define-an-extension-trait.md b/src/idiomatic/leveraging-the-type-system/extension-traits/should-i-define-an-extension-trait.md new file mode 100644 index 000000000000..e8d367ec1221 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/extension-traits/should-i-define-an-extension-trait.md @@ -0,0 +1,40 @@ +--- +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**. + +
+ +- 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. + +
diff --git a/src/idiomatic/leveraging-the-type-system/extension-traits/trait-method-conflicts.md b/src/idiomatic/leveraging-the-type-system/extension-traits/trait-method-conflicts.md new file mode 100644 index 000000000000..6379b03a8dbc --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/extension-traits/trait-method-conflicts.md @@ -0,0 +1,64 @@ +--- +minutes: 5 +--- + +# Trait Method Conflicts + +What happens when you have a name conflict between two different trait methods +implemented for the same type? + +```rust,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; +pub use ext::Ext2; + +// Which method is invoked? +// The one from `Ext1`? Or the one from `Ext2`? +assert!("dad".is_palindrome()); +``` + +
+ +- The trait you are extending may, in a newer version, add a new trait method + with the same name of 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. + + 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? + +- 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. For + example, you can call `Ext1::is_palindrome("dad")` or + `Ext2::is_palindrome("dad")`. + + For methods with more complex signatures, you may need to use a more explicit + [fully-qualified syntax][1]. + +
+ +[1]: https://doc.rust-lang.org/reference/expressions/call-expr.html#disambiguating-function-calls