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 01/16] 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 1950476a423a..bc121993d98a 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 000000000000..de44b5ba1b0d --- /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 000000000000..a56446f4c2e4 --- /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 000000000000..99a9f71d9342 --- /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 000000000000..a06ea97b07b3 --- /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 From 408962cda55e612bc625a4c3bd74fd9c8b6f5472 Mon Sep 17 00:00:00 2001 From: LukeMathWalker <20745048+LukeMathWalker@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:11:48 +0200 Subject: [PATCH 02/16] Reword --- .../leveraging-the-type-system/extension-traits.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/idiomatic/leveraging-the-type-system/extension-traits.md b/src/idiomatic/leveraging-the-type-system/extension-traits.md index de44b5ba1b0d..a502bd759995 100644 --- a/src/idiomatic/leveraging-the-type-system/extension-traits.md +++ b/src/idiomatic/leveraging-the-type-system/extension-traits.md @@ -19,10 +19,10 @@ 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. +- 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. + 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_. From 1670b6468c86be8bfb96b53452ed14be5457f598 Mon Sep 17 00:00:00 2001 From: LukeMathWalker <20745048+LukeMathWalker@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:36:42 +0200 Subject: [PATCH 03/16] Use consistent terminology --- .../extension-traits/method-resolution-conflicts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index a06ea97b07b3..e899b913a69c 100644 --- 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 @@ -51,8 +51,8 @@ assert_eq!(" 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. + `&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 From bd1b26db22327b73f753c25985628ba3344b509d Mon Sep 17 00:00:00 2001 From: LukeMathWalker <20745048+LukeMathWalker@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:15:18 +0200 Subject: [PATCH 04/16] Extending other traits --- src/SUMMARY.md | 2 +- .../extending-foreign-traits.md | 7 -- .../extending-other-traits.md | 90 +++++++++++++++++++ 3 files changed, 91 insertions(+), 8 deletions(-) delete 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-other-traits.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index bc121993d98a..bedd1b5dd2e8 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -440,7 +440,7 @@ - [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) + - [Extending Other Traits](idiomatic/leveraging-the-type-system/extension-traits/extending-other-traits.md) --- 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 deleted file mode 100644 index a56446f4c2e4..000000000000 --- a/src/idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-traits.md +++ /dev/null @@ -1,7 +0,0 @@ -# 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-other-traits.md b/src/idiomatic/leveraging-the-type-system/extension-traits/extending-other-traits.md new file mode 100644 index 000000000000..0a2562cefe97 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/extension-traits/extending-other-traits.md @@ -0,0 +1,90 @@ +--- +minutes: 10 +--- + +# Extending Other Traits + +Extension traits can 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. + +- 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`. + +
+ +- 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 + +[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 From 06f251e78c143ac30c749560781d82f900d9cb19 Mon Sep 17 00:00:00 2001 From: LukeMathWalker <20745048+LukeMathWalker@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:25:43 +0200 Subject: [PATCH 05/16] Trait method conflicts --- src/SUMMARY.md | 1 + .../extending-other-traits.md | 6 +- .../trait-method-resolution-conflicts.md | 62 +++++++++++++++++++ 3 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/idiomatic/leveraging-the-type-system/extension-traits/trait-method-resolution-conflicts.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index bedd1b5dd2e8..3e81b0c014ed 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -441,6 +441,7 @@ - [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 Other Traits](idiomatic/leveraging-the-type-system/extension-traits/extending-other-traits.md) + - [Trait Method Resolution Conflicts](idiomatic/leveraging-the-type-system/extension-traits/trait-method-resolution-conflicts.md) --- 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 index 0a2562cefe97..ecbc89a028a2 100644 --- 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 @@ -1,5 +1,5 @@ --- -minutes: 10 +minutes: 15 --- # Extending Other Traits @@ -77,10 +77,6 @@ assert_eq!(true.quoted(), "'true'");
-- 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 - [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 diff --git a/src/idiomatic/leveraging-the-type-system/extension-traits/trait-method-resolution-conflicts.md b/src/idiomatic/leveraging-the-type-system/extension-traits/trait-method-resolution-conflicts.md new file mode 100644 index 000000000000..ac8a6a6b4c21 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/extension-traits/trait-method-resolution-conflicts.md @@ -0,0 +1,62 @@ +--- +minutes: 5 +--- + +# Trait Method Resolution Conflicts + +What happens when you have a name conflict between two different trait methods +implemented for the same type? + +```rust +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 extended trait may, in a newer version, add a new trait 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? + +- 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 From 21a105e147cb69851a15bf7754c5616870050dd8 Mon Sep 17 00:00:00 2001 From: LukeMathWalker <20745048+LukeMathWalker@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:36:16 +0200 Subject: [PATCH 06/16] Shorter title --- src/SUMMARY.md | 2 +- ...method-resolution-conflicts.md => trait-method-conflicts.md} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/idiomatic/leveraging-the-type-system/extension-traits/{trait-method-resolution-conflicts.md => trait-method-conflicts.md} (97%) diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 3e81b0c014ed..fd6ab1659d7f 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -441,7 +441,7 @@ - [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 Other Traits](idiomatic/leveraging-the-type-system/extension-traits/extending-other-traits.md) - - [Trait Method Resolution Conflicts](idiomatic/leveraging-the-type-system/extension-traits/trait-method-resolution-conflicts.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/trait-method-resolution-conflicts.md b/src/idiomatic/leveraging-the-type-system/extension-traits/trait-method-conflicts.md similarity index 97% rename from src/idiomatic/leveraging-the-type-system/extension-traits/trait-method-resolution-conflicts.md rename to src/idiomatic/leveraging-the-type-system/extension-traits/trait-method-conflicts.md index ac8a6a6b4c21..95415b905294 100644 --- a/src/idiomatic/leveraging-the-type-system/extension-traits/trait-method-resolution-conflicts.md +++ b/src/idiomatic/leveraging-the-type-system/extension-traits/trait-method-conflicts.md @@ -2,7 +2,7 @@ minutes: 5 --- -# Trait Method Resolution Conflicts +# Trait Method Conflicts What happens when you have a name conflict between two different trait methods implemented for the same type? From b64886c7f9c4e57a5c54218460f37569e4f8547b Mon Sep 17 00:00:00 2001 From: LukeMathWalker <20745048+LukeMathWalker@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:45:18 +0200 Subject: [PATCH 07/16] Mark trait method conflict as compile_fail --- .../extension-traits/trait-method-conflicts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 95415b905294..91817a8e9e9b 100644 --- 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 @@ -7,7 +7,7 @@ minutes: 5 What happens when you have a name conflict between two different trait methods implemented for the same type? -```rust +```rust,compile_fail mod ext { pub trait Ext1 { fn is_palindrome(&self) -> bool; From c36f7fe24d6b4758d259700d554ea8ce27d1b009 Mon Sep 17 00:00:00 2001 From: LukeMathWalker <20745048+LukeMathWalker@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:36:59 +0200 Subject: [PATCH 08/16] Fix link --- .../extension-traits/extending-foreign-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 99a9f71d9342..187274be92f4 100644 --- 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 @@ -76,5 +76,5 @@ assert!(!"grandma".is_palindrome()); -[1]: https://rust-lang.github.io/rfcs/0445-extension-trait-rfc.html +[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 From 5ab37d8272329f7114f576c4ecee79a7dcb0c226 Mon Sep 17 00:00:00 2001 From: Luca Palmieri <20745048+LukeMathWalker@users.noreply.github.com> Date: Tue, 15 Jul 2025 14:38:57 +0200 Subject: [PATCH 09/16] Update src/idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md Co-authored-by: Dmitri Gribenko --- .../extension-traits/extending-foreign-types.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 187274be92f4..5a3641467cb0 100644 --- 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 @@ -20,9 +20,9 @@ mod ext { } } -// Bring the extension trait into scope.. +// Bring the extension trait into scope... pub use ext::StrExt as _; -// ..then invoke its methods as if they were inherent methods +// ...then invoke its methods as if they were inherent methods assert!("dad".is_palindrome()); assert!(!"grandma".is_palindrome()); ``` From 841bce5f329a4c564757e992e38a3fe82ef7de97 Mon Sep 17 00:00:00 2001 From: Luca Palmieri <20745048+LukeMathWalker@users.noreply.github.com> Date: Tue, 15 Jul 2025 14:47:36 +0200 Subject: [PATCH 10/16] Update src/idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md Co-authored-by: Dmitri Gribenko --- .../extension-traits/method-resolution-conflicts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index e899b913a69c..b19c520b9ddb 100644 --- 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 @@ -41,7 +41,7 @@ assert_eq!(" dad ".trim_ascii(), "dad"); - [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`. + 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: From aa2ab0fd705b1fbc72746c9283f3391df14e7cbc Mon Sep 17 00:00:00 2001 From: Luca Palmieri <20745048+LukeMathWalker@users.noreply.github.com> Date: Tue, 15 Jul 2025 14:47:49 +0200 Subject: [PATCH 11/16] Update src/idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md Co-authored-by: Dmitri Gribenko --- .../extension-traits/method-resolution-conflicts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index b19c520b9ddb..c6af0ebfb556 100644 --- 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 @@ -65,7 +65,7 @@ assert_eq!(" dad ".trim_ascii(), "dad"); ## 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 + 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. From 63d3aa33fdb94d6f8f18c9cde72c4c311b2f607d Mon Sep 17 00:00:00 2001 From: Luca Palmieri <20745048+LukeMathWalker@users.noreply.github.com> Date: Tue, 15 Jul 2025 14:48:07 +0200 Subject: [PATCH 12/16] Update src/idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md Co-authored-by: Dmitri Gribenko --- .../extension-traits/method-resolution-conflicts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index c6af0ebfb556..68378915636a 100644 --- 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 @@ -39,7 +39,7 @@ assert_eq!(" dad ".trim_ascii(), "dad"); 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 + 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`. From 17ba065295a8e0007a29ac1f41a599bc8eed7e65 Mon Sep 17 00:00:00 2001 From: LukeMathWalker <20745048+LukeMathWalker@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:52:18 +0200 Subject: [PATCH 13/16] Address review comments --- .../extension-traits.md | 44 +++++++++++++++---- .../extending-foreign-types.md | 14 ++++-- .../trait-method-conflicts.md | 6 ++- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/idiomatic/leveraging-the-type-system/extension-traits.md b/src/idiomatic/leveraging-the-type-system/extension-traits.md index a502bd759995..1e8380f09836 100644 --- a/src/idiomatic/leveraging-the-type-system/extension-traits.md +++ b/src/idiomatic/leveraging-the-type-system/extension-traits.md @@ -1,10 +1,14 @@ --- -minutes: 5 +minutes: 15 --- # Extension Traits -In Rust, you can't define new inherent methods for foreign types. +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 // 🛠️❌ @@ -15,10 +19,22 @@ impl &'_ str { } ``` -You can use the **extension trait pattern** to work around this limitation. +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 @@ -26,12 +42,22 @@ You can use the **extension trait pattern** to work around this limitation. - 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. + 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 suprising or otherwise + unexpected behavior. If disambiguation happens explicitly, it can increase the + cognitive load on developers who are reading your code. - 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. + 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 index 5a3641467cb0..83a30d32c149 100644 --- 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 @@ -48,9 +48,16 @@ assert!(!"grandma".is_palindrome()); 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. +- 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. - Some students may be wondering: does the extension trait pattern provide enough value to justify the additional boilerplate? Wouldn't a free function @@ -78,3 +85,4 @@ assert!(!"grandma".is_palindrome()); [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/trait-method-conflicts.md b/src/idiomatic/leveraging-the-type-system/extension-traits/trait-method-conflicts.md index 91817a8e9e9b..6379b03a8dbc 100644 --- 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 @@ -40,8 +40,10 @@ assert!("dad".is_palindrome());
-- The extended trait may, in a newer version, add a new trait method with the - same name of our extension method. +- 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 From d10c9859fca1435d9b286929919925be3f162470 Mon Sep 17 00:00:00 2001 From: LukeMathWalker <20745048+LukeMathWalker@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:55:45 +0200 Subject: [PATCH 14/16] Formatting and typos --- .../leveraging-the-type-system/extension-traits.md | 2 +- .../extension-traits/method-resolution-conflicts.md | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/idiomatic/leveraging-the-type-system/extension-traits.md b/src/idiomatic/leveraging-the-type-system/extension-traits.md index 1e8380f09836..f5bc3025b189 100644 --- a/src/idiomatic/leveraging-the-type-system/extension-traits.md +++ b/src/idiomatic/leveraging-the-type-system/extension-traits.md @@ -47,7 +47,7 @@ pattern** to work around this limitation. 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 suprising or otherwise + 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. 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 index 68378915636a..df91c0e58360 100644 --- 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 @@ -65,10 +65,9 @@ assert_eq!(" dad ".trim_ascii(), "dad"); ## 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. + 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.
From 771a37a8c9c561a4d149f52d4a9ac8bcc39332ca Mon Sep 17 00:00:00 2001 From: LukeMathWalker <20745048+LukeMathWalker@users.noreply.github.com> Date: Thu, 31 Jul 2025 17:09:40 +0200 Subject: [PATCH 15/16] Elaborate further on the desired goal when extending other traits --- .../extension-traits/extending-other-traits.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 index ecbc89a028a2..d8d4e8ba34fd 100644 --- 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 @@ -4,7 +4,8 @@ minutes: 15 # Extending Other Traits -Extension traits can attach new methods to _all_ implementors of a given trait: +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 { @@ -41,6 +42,14 @@ assert_eq!(true.quoted(), "'true'"); 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`. From b58a2a52135dcb259e8a433361483ec387565723 Mon Sep 17 00:00:00 2001 From: LukeMathWalker <20745048+LukeMathWalker@users.noreply.github.com> Date: Thu, 31 Jul 2025 17:16:52 +0200 Subject: [PATCH 16/16] Extract bullet point into its own slide --- src/SUMMARY.md | 1 + .../extending-foreign-types.md | 24 +---------- .../should-i-define-an-extension-trait.md | 40 +++++++++++++++++++ 3 files changed, 42 insertions(+), 23 deletions(-) create mode 100644 src/idiomatic/leveraging-the-type-system/extension-traits/should-i-define-an-extension-trait.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index fd6ab1659d7f..6c22e06eff86 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -440,6 +440,7 @@ - [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/extending-foreign-types.md b/src/idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md index 83a30d32c149..bf781700f09a 100644 --- 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 @@ -1,5 +1,5 @@ --- -minutes: 15 +minutes: 10 --- # Extending Foreign Types @@ -59,28 +59,6 @@ assert!(!"grandma".is_palindrome()); Since extension traits aren't meant to be used in `where` clauses, they are conventionally imported via an underscore import. -- 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-conventions.html 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. + +