Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
be796c3
Extension traits
LukeMathWalker Jul 8, 2025
408962c
Reword
LukeMathWalker Jul 9, 2025
1670b64
Use consistent terminology
LukeMathWalker Jul 14, 2025
bd1b26d
Extending other traits
LukeMathWalker Jul 14, 2025
06f251e
Trait method conflicts
LukeMathWalker Jul 14, 2025
21a105e
Shorter title
LukeMathWalker Jul 14, 2025
b64886c
Mark trait method conflict as compile_fail
LukeMathWalker Jul 14, 2025
c36f7fe
Fix link
LukeMathWalker Jul 15, 2025
5ab37d8
Update src/idiomatic/leveraging-the-type-system/extension-traits/exte…
LukeMathWalker Jul 15, 2025
841bce5
Update src/idiomatic/leveraging-the-type-system/extension-traits/meth…
LukeMathWalker Jul 15, 2025
aa2ab0f
Update src/idiomatic/leveraging-the-type-system/extension-traits/meth…
LukeMathWalker Jul 15, 2025
63d3aa3
Update src/idiomatic/leveraging-the-type-system/extension-traits/meth…
LukeMathWalker Jul 15, 2025
17ba065
Address review comments
LukeMathWalker Jul 31, 2025
d10c985
Formatting and typos
LukeMathWalker Jul 31, 2025
771a37a
Elaborate further on the desired goal when extending other traits
LukeMathWalker Jul 31, 2025
b58a2a5
Extract bullet point into its own slide
LukeMathWalker Jul 31, 2025
d141a8d
Apply suggestions from code review
tall-vase Oct 14, 2025
a0146cc
Address feedback + grammar check
Oct 16, 2025
631e8f4
Merge branch 'main' into extension-traits
tall-vase Oct 16, 2025
85a2e5f
Space out paragraphs
tall-vase Oct 20, 2025
469a040
Address feedback wrt demonstration of trait method conflict slide
Oct 20, 2025
3d17013
Update src/idiomatic/leveraging-the-type-system/extension-traits/trai…
tall-vase Oct 20, 2025
669c74a
Formatting pass
Oct 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down
37 changes: 37 additions & 0 deletions src/idiomatic/leveraging-the-type-system/extension-traits.md
Original file line number Diff line number Diff line change
@@ -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.

<details>

- 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_.

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.

</details>
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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());
```

<details>

- 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.

</details>

[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
Original file line number Diff line number Diff line change
@@ -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");
```

<details>

- 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.

</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