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