Skip to content

Extension traits #2812

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open

Conversation

LukeMathWalker
Copy link
Collaborator

@LukeMathWalker LukeMathWalker commented Jul 9, 2025

No description provided.

@LukeMathWalker LukeMathWalker changed the base branch from main to idiomatic-rust July 9, 2025 17:07
@LukeMathWalker LukeMathWalker changed the base branch from idiomatic-rust to main July 9, 2025 19:03
@LukeMathWalker LukeMathWalker force-pushed the extension-traits branch 2 times, most recently from 9ad9fa7 to 9b50d50 Compare July 9, 2025 19:12
Copy link
Collaborator

@djmitche djmitche left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking good!

@LukeMathWalker LukeMathWalker marked this pull request as ready for review July 14, 2025 15:37
@LukeMathWalker LukeMathWalker requested a review from djmitche July 14, 2025 17:20
Copy link
Collaborator

@randomPoison randomPoison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good! I only have a couple of minor comments, but on the whole I like it :)


# Extension Traits

In Rust, you can't define new inherent methods for foreign types.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "foreign type" a standard term to refer to types from other crates? I feel like it can be easily misinterpreted as "types defined in C++" (similar to "foreign functions"). Is there an alternative?

Copy link
Collaborator Author

@LukeMathWalker LukeMathWalker Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Foreign type" and "foreign trait" are the terms used in the Rust reference when discussing orphan rules (see this section). So I'd say they are standard terms in this specific context.

Alternatively, we could use "new inherent methods for a type defined in another crate" as an alternative phrasing. It could get fairly verbose in the speaker notes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack. Maybe then briefly mention in the speaker notes what these terms mean, hinting that the instructor should maybe explain them. I don't think it is a given that the audience understands coherence rules and understands why it matters where the trait is defined.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 17ba065


<details>

- Compile the example to show the compiler error that's emitted.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should start by explaining what we want to achieve first: we want the user to be able to write something like mystr.is_palindrome(). Then transition to the obvious solution that does not work (the code snippet above). And then say that this is why we are using a more complex solution that does work.

Otherwise it might be confusing to some audience members: we are starting a new chapter by looking at a piece of code that does not compile (the impl block above), we want it to compile (why?..), but we actually wouldn't, instead we should do something else.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I restructured the flow a bit in 17ba065. What do you think?

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_.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can raise the level of abstraction even higher.

One effective way to approach evaluating features in programming design is to ask "what if everybody did this?"

"I want to be able to add methods to someone else's type! I want to add an is_palindrome method to a string" - "Yes, but what if two people did this?"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 17ba065

LukeMathWalker and others added 4 commits July 15, 2025 14:38
…nding-foreign-types.md

Co-authored-by: Dmitri Gribenko <[email protected]>
…od-resolution-conflicts.md

Co-authored-by: Dmitri Gribenko <[email protected]>
…od-resolution-conflicts.md

Co-authored-by: Dmitri Gribenko <[email protected]>
…od-resolution-conflicts.md

Co-authored-by: Dmitri Gribenko <[email protected]>
Comment on lines 43 to 44
- The extended trait may, in a newer version, add a new trait method with the
same name of our extension method.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- The extended trait may, in a newer version, add a new trait method with the
same name of our extension method.
- Another extension trait may, in a newer version, add a new trait method with the
same name as our extension method.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both cases are actually possible, so I reworded the paragraph to account for both in 17ba065

@LukeMathWalker LukeMathWalker requested a review from gribozavr July 31, 2025 15:17
Copy link
Collaborator

@randomPoison randomPoison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might want to change the examples in this section to not be on &str, since implementing a trait on a reference leads to confusing behavior around method resolution (which I talk about in more detail in one of the comments here). I think things would be less confusing if we used a non-reference type like i32 or struct Foo.

Comment on lines +53 to +55
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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is an accurate explanation of what's happening here. The reference explicitly states (in the info box in that section) that &self methods have higher priority than &mut self methods.

I think the reason why the &mut self version gets higher priority here is that the receiver expression is &mut &str. If I'm understanding the reference's explanation of method resolution correctly, this means that when it builds the list of candidate receiver types, &mut &str is the first candidate type in the list. It's then choosing between the inherent &str method and the &mut &str method coming from the trait, and the latter wins because it's the actual type of the expression &mut " dad ".

I think the confusion here is because we're implementing the trait on on &str, which is already a reference type. If I change the trait to be implemented on str directly (i.e. impl StrExt for str), when when I change the method to take &mut self the inherent method still gets called (exmple in the playground). Part of the reason for this is because when we do (&mut " dad ") we're not getting a &mut str, we're getting a &mut &str.

I think things would be a lot less ambiguous if we were demonstrating this on a regular, non-reference type such as i32 or struct Foo.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for looking at it closely!
Re-reading through it, and cross-referencing with the RFC, I agree with your interpretation as to why things play out as they do in terms of precedence. I'll rework the example to something simpler.

Comment on lines +57 to +59
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].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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].
Point the students to the Rust reference for more information on
[method resolution][2].

I think we can just link to the reference, I don't think linking to an open PR is necessary. Eventually the things in that PR will (hopefully) land, so just linking to the reference is enough imo.

Comment on lines +52 to +60
- 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].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably worth showing the full syntax to students, since sometimes Trait::method(foo) isn't enough. Specifically, if the compiler can't infer the type of foo then it won't be able to resolve which type's trait implementation to use. In those cases you'd have to write <Type as Trait>::method(foo). That syntax can be surprising for people new to the language (I know I was confused the first time I saw it), so I think showing it here (and explaining when it's necessary) would be good.

Copy link
Collaborator

@djmitche djmitche left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me. The note below is stylistic and relates only to the speaker notes, so feel free to treat it as a mild preference at most.

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?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the approach of surveying the class is something specific to the instructor and situation. I would do this without prompting for small-group, in-person instruction, but for a large or shy group, or when using Zoom or Meet, it's not a very effective strategy (it results in lots of empty air).

So, it's a very minor point and not worth changing, but I think the question in the comment is sufficient here, and does not need repeating in the notes. Instructors will prompt from the comment if appropriate, or address it in a manner appropriate to the context and their teaching style.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants