From be796c36813c5b2f33015c518248ff1905e16d2f Mon Sep 17 00:00:00 2001 From: LukeMathWalker <20745048+LukeMathWalker@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:02:42 +0200 Subject: [PATCH] Extension traits --- src/SUMMARY.md | 4 + .../extension-traits.md | 37 +++++++++ .../extending-foreign-traits.md | 7 ++ .../extending-foreign-types.md | 80 +++++++++++++++++++ .../method-resolution-conflicts.md | 79 ++++++++++++++++++ 5 files changed, 207 insertions(+) create mode 100644 src/idiomatic/leveraging-the-type-system/extension-traits.md create mode 100644 src/idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-traits.md create mode 100644 src/idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md create mode 100644 src/idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 1950476a..bc121993 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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) --- 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 00000000..de44b5ba --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/extension-traits.md @@ -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. + +
+ +- 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. + +
diff --git a/src/idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-traits.md b/src/idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-traits.md new file mode 100644 index 00000000..a56446f4 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-traits.md @@ -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 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 00000000..99a9f71d --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.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()); +``` + +
+ +- 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. + +
+ +[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 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 00000000..a06ea97b --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md @@ -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"); +``` + +
+ +- 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. + +
+ +[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