-
Notifications
You must be signed in to change notification settings - Fork 1.7k
RFC: obj-action style method disambiguation #3908
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
Conversation
| img.OtherOps::rotate(); // Calls via Alias -> Transform (Generic) | ||
| ``` | ||
|
|
||
| The `Self` keyword is implicitly treated as an alias for the inherent implementation, ensuring symmetry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think having some explicit syntax that calls inherent methods or errors and never tries to call trait methods is the best part of this RFC, I've often wanted that for code like:
impl MyType {
pub const fn foo(&self) -> Bar { ... }
}
impl SomeTrait for MyType {
fn foo(&self) -> Bar {
// old syntax `self.foo()` is problematic since it turns into
// infinite recursion if the inherent foo method is renamed/removed.
// it also can be confusing to read since you have to know/guess
// there's an inherent method `foo`
// unambiguously call inherent method, will error if the
// inherent foo method is renamed/removed rather than cause infinite recursion
self.Self::foo()
}
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think there is a need for Native trait alias? The reason is that some methods may not be strictly inherent but could be treated as if they were inherent when used outside the crate’s source code. By default, it would allow calling only methods defined in the crate where the type originates, but this could also be overridden in the implementation to be a trait that incorporates all the traits essential to the type.
Probably there is a better name for the alias
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think there is a need for
Nativetrait alias? The reason is that some methods may not be strictly inherent but could be treated as if they were inherent when used outside the crate’s source code. By default, it would allow calling only methods defined in the crate where the type originates, but this could also be overridden in the implementation to be a trait that incorporates all the traits essential to the type.Probably there is a better name for the alias
It would be useful when you implement your own Trait for a Type from some library, and Type has a method called the same as the one you implemented. You try value.Self:method(args): expecting old behaviour and the compiler says that Type doesn't have an inherent method called method while it still being a part of the library and old behaviour
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currently in order for you to call a trait method the trait has to be in scope either via use or by bounds you wrote on some generic. so there currently aren't really trait methods that act like inherent methods, they're instead just traits that are in-scope.
it has been proposed to have inherent traits -- which make the trait methods behave like inherent methods -- but that isn't part of rust yet. if/when those are added, having them be accessible using a.Self::foo syntax might be nice, since by declaring the impl #[inherent] the author of the type explicitly is including all those trait methods in the inherent API of the type and presumably knows there aren't any problematic method name conflicts between those traits and the regular inherent methods.
so I don't think a.Native::foo is necessary since there isn't currently anything that can't just use a.Self::foo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
while I'm thinking about it, it would be useful to have an explicit syntax for inherent associated functions that don't necessarily have a self argument, so in addition to your proposed v.Self::foo() syntax I think we should also have <Ty as Self>::foo syntax as was mentioned on Zulip.
e.g.:
pub struct S;
impl S {
pub fn foo() { ... }
pub fn bar(v: i32) -> i32 { ... }
}
impl Trait for S {
fn foo() {
<S as Self>::foo();
}
}
pub fn f(v: Option<i32>) -> Option<i32> {
v.map(<S as Self>::bar)
}| * If `Ident` matches a `pub use Trait as Alias;` statement, the call resolves to `<Type as Trait>::method`. | ||
| * The keyword `Self` is implicitly treated as an alias for the inherent implementation. `obj.Self::method()` resolves to the inherent method. | ||
|
|
||
| 3. **Inherent Impl Items**: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
imo a use Trait; in an impl block should also make paths like path::to::MyType::Trait valid wherever you might want to have a path to a trait, not just in method resolution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't that the same? or you mean use can refer to traits for libraries that are not imported explicitly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've finally understood. Yeah, I also think so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(edit: didn't see your comment until after I posted this)
I mean you can have in addition to the a.Trait::foo() syntax:
pub struct MyType;
impl MyType {
pub use SomeTrait as Trait;
}
// now we can use MyType::Trait:
impl MyType::Trait for Foo {
type Ty = String;
}
pub fn bar(a: impl MyType::Trait<Ty = ()>, b: &dyn MyType::Trait<Ty = u8>) -> <() as MyType::Trait>::Ty {
todo!()
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imo MyType::Trait should not be used over SomeModule::SomeTrait if we know that SomeModule::SomeTrait exists and what it is, e.g. in impl blocks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But there is no reason to restrict this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, it could be useful for when you're writing some proc-macro that needs to access traits based off of syntax like MyType { field1: ... } so the macro can then generate code like <() as <MyType>::Field1>::field_properties()
| * It mirrors C++ explicit qualification (e.g., `obj.Base::method()`). | ||
| * **Why Parentheses for Ad-hoc?** | ||
| * `obj.Trait::method` is syntactically ambiguous with field access. | ||
| * `obj.(Trait::method)` is unambiguous and visually distinct. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(val.field)(args) is already extant in the language as a way to explicitly use a field that has a function pointer, so while this is technically unambiguous, it is close to an existing syntax that it could be confused with. I am not sure I would call it "visually distinct". Visually distinct from the other call approach you want to introduce, maybe, but even then I'm not so sure.
I feel like your RFC breezes by the complexity of the current situation, when it should consider where it can incur more syntactic or semantic confusion or difficult-to-adjudicate edge cases: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=6bf8135485fb1c929848c8797ba4d360
Yes, I know that usually you don't have three things named the same way. I am just using this kind of worst-case scenario to illustrate, because the reality can trend closer to the worst-case scenarios than we would like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've replaced the reason for the parentheses with a better one. As for visual distinction, if you don't split your code over multiple lines properly, parentheses don't look pleasant in any case of their usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Visual distinction often is the opposite of pleasant, if the contrast is sufficiently harsh, so my critique of visual distinction is not about whether it looks nice.
| 1. **Cognitive Load**: The user must stop writing logic, look up the full trait path, import it, and restructure the code to wrap the object in a function call. | ||
| 2. **API Opacity**: Consumers often do not know which specific module a trait comes from, nor should they need to manage those imports just to call a method. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this apply to the entire RFC as motivation? I get friction from lots of things, but if I am calling something on purpose I generally do know where it is, because in order to call it I must first have the idea that such a function may exist to call. Because I am not that imaginative, this "idea" usually comes from looking at the docs or source code, or via a suggestion via tools that can find it for me, like rustc or rust-analyzer. The LSP can even handle importing it for me. So this part of the motivation seems weak, because without an import, most people will not want to write
proc
.(std::os::unix::process::CommandExt::pre_exec)(func).
.(std::os::unix::process::CommandExt::exec)();They will instead want to use std::os::unix::process::CommandExt; still.
Now, if this justification applies entirely to definition-site aliases, the question then becomes what the motivation is for ad-hoc disambiguation? "Quick fixes" alone? Is that worth adding it to the language, considering its other drawbacks, like "having similar-looking syntax for the same call that can dispatch to entirely different traits"?
It may be better to cut this RFC in half.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main motivation is that currently, when you need to resolve a method name conflict, you’re forced to rewrite the call as a standalone function call, which breaks method chaining.
My RFC proposes two solutions:
- Happy path: The author of the struct has provided an alias for the conflicting method and you can just use it
- Unhappy path: There is no alias, so you have to take the longer route: identify the trait, bring it into scope (or fully qualify it), and use parentheses to explicitly signal that method resolution is being done by an external way, outside the object’s type implementation.
|
I'm gonna rewrite the RFC because I realized how overcomplicated it is and the fact that it's not one small incremental step but rather two or three. |
|
I'll do it tomorrow, it's too late for me I am gonna leave only I started writing this example but the compiler appeared to be too smart. |
I don't know what kind of intelligence I've noticed in defaulting to the inherent method. |
|
I tried to illustrate this in https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=6bf8135485fb1c929848c8797ba4d360 so you could meditate on it when improving the design (or just editing the proposal, whichever). If a value in scope is of a type that "inherently" impls a function, and that function is called using method syntax, the compiler attempts to first select the inherent impl, even if a trait would be valid to call. Using Using When dealing with a generic |
|
We already have syntax for accessing only inherent methods: |
|
I've rewritten the RFC. I hope it's cleaner this time |
|
I hope the PR will be reviewed today. A lot has changed |
| [unresolved-questions]: #unresolved-questions | ||
|
|
||
| * **Syntax Choice**: Should we consider other bracket types to avoid confusion with tuple grouping? (e.g., `obj.{Trait::method}()` or `obj.[Trait::method]()`). | ||
| * **Syntax Choice**: Should we consider other bracket types to avoid confusion with tuple grouping? (e.g., `obj.{Trait::method}()` or `obj.[Trait::method]()`)? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's even more confusing.
[] is for slices/arrays, and {} is for blocks.
() is already general enough for us to add more uses to it.
This just my opinion, though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I also prefer () but someone on the forum proposed <> so I had put it there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should change the examples though to include <>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My two cents:
- Angle brackets are currently used as "parentheses for types", eg.
<Type as Trait>::method()or<[T; N]>::method(). Here, they would be used to enclose a path that refers to a term, not a type, butobj.<path::to::Trait::method>()still feels somehow natural to me. - But wait, wouldn't the normal
obj.<path::to::Trait>::method()already work here, without any new disambiguation syntax? The brackets are needed anyway in cases likeobj.<Trait<i32>>::method()(or turbofish might be used?) - Simply using normal parentheses has precedence in expressions like
(T::function)(), even though they're redundant there. - Curly brackets are not my favorite, but one could argue that
use foo::{bar::function}counts as a sort of precedent. - Square brackets don't really match any existing syntax.
|
Are @programmerjake and @workingjubilee going to do a second review? Just so I know what to expect |
what github shows as a review (unless it's actually marked approved or changes requested) basically just means we used the review functionality github has as just a way to group comments together, you shouldn't read anything into it being marked as a review. we'll comment if/when we have more to say. as for me, nothing particularly stands out as objectionable so I'm letting others comment. |
| * **Why Parentheses for Trait Method Calls?** | ||
| * `value.Trait::method` looks like there is something called `Trait` inside the `value` while `Trait` is coming from the scope of the call. | ||
| * `value.(Trait::method)` shows that `Trait::method` is evaluated first and then applied to the `value`. | ||
| * **Reservation**: We specifically reserve the unparenthesized syntax `value.Category::method()` (where `Category` is not `Self`) for possible future language features, such as "Categorical" or "Facet" views of an object. Using parentheses around trait paths avoids closing the door on this design space. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will try again to convey my previous comment. Right now, () has two definitely-semantic meanings:
- tuple construction
- function calls
Otherwise, () is almost purely a form of disambiguation and can be omitted if you restructure the code enough via ways that "don't change anything". Usually, "add another let-binding", which does have some semantic effect due to things like introducing new coercion sites but for most nested expressions this is equivalent-enough. In particular, the compiler does elaborate Rust into a reduced form of the language that is, indeed, a series of very explicit let-bindings and then operations on the places those let-bindings describe, and often includes many new let-bindings to account for temporaries it must create to evaluate nested expressions.
The field-function-pointer-call is the closest to a legitimate semantic difference, but it can also be removed via a let-binding of a temporary, as can most forms of binary operator disambiguation: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=99c858d44b35917f022d9d3eb7a2805f
let something = Something { whatever: something };
something.whatever(); // inherent impl call
(something.whatever)(); // field fnptr call
let whatever_ptr = something.whatever;
whatever_ptr(); // equivalence to form with explicit let-bindingIf we accept this new feature, and then add another feature that even the author sees as "more desirable", we then can have potentially large semantic differences based on the presence or absence of parentheses in otherwise-equivalent-looking expressions. We will also be nudging people towards using a particular one, making it more likely that people overlook one for another entirely. So we will have made the language larger in two ways that then are easily confused with each other. That's not great for teachability.
I think it would be better if you ask for what you really want, unless you can offer a single, easily-described resolution rule that covers both use-cases you have in mind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
struct Type {
field: String,
}
fn main() {
let value_a = Type {field: "lol".to_string()};
let value_b = Type {field: "olo".to_string()};
let method1 = |this: &Type| {println!("method1: {}", this.field)};
let method2 = |this: &Type| {println!("method2: {}", this.field)};
use {method1, method2} for {value1, value2} /*or `Type`*/;
value_a.method1();
value_b.method1();
value_b.method2();
}What do you think about this syntax as an extension of the "Scoped Prioritization" idea in the "Future Possibilities" section? Does it solve one of the issues you mentioned?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{method1, method2, ...} would mean that method1 and method2 are used alongside with everything previously available for Type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be useful to consider how #3530 will affect (or not) actual uses if it is accepted alongside this RFC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR is not moving. Imo its proposed feature eventually will be implemented via use for syntax which will be my next RFC if this gets merged. use for seems to be very flexible.
| * **Parser Complexity**: The parser requires lookahead or distinct rules to distinguish `.` followed by `(` (method call) versus `.` followed by `Self` followed by `::`. | ||
| * **Punctuation Noise**: The syntax `.(...)` introduces more "Perl-like" punctuation symbols to the language, which some may find unaesthetic. | ||
|
|
||
| ## Rationale and alternatives |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternative syntax could be something like:
(b as Reset).reset(); // calls Reset::reset
(b as Self).reset(); // calls Self::resetThis is parallel to the syntax for disambiguating in UFC:
<Builder as Reset>::reset(builder)The possible downside is it looks like a cast, even though the type it is cast to isn't really a valid type to cast to (you can't have a bare trait value).
Also, what if builder is actually a &Builder? Would you instead us as Reset or still as Reset? Same for &mut.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, what if builder is actually a &Builder? Would you instead us as Reset or still as Reset? Same for &mut.
I don't know, create your own RFC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
create your own RFC.
My point is that the (b as Reset).reset() syntax could/should be included as an alternative. And, assuming you prefer your proposal to my (very rough) proposal here, say why your proposal is a better option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Compare
obj
.Self::chain()
.(Trait1::of)()
.(Trait2::method)()
.(Trait1::calls)();to
(((obj
as Self).chain()
as Trait1).of()
as Trait2).method()
as Trait1).calls();|
|
||
| * **Why No Parentheses for Inherent Method Call?** | ||
| * Unlike `Trait`, which comes from the outer scope, `Self` conceptually belongs to the instance itself. | ||
| * `value.Self::method()` aligns with the mental model that `Self` is intrinsic to `value`, acting as a specific "facet" of the object itself, rather than an external function applied to it. This justifies the lack of parentheses, matching the reserved `value.Category::method()` syntax. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, I think it is confusing that this works differently than specifying the trait. I think consistency between Self and traits is more valuable than the "intrinsicness" of Self.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Unlike `Trait`, which comes from the outer scope, `Self` conceptually belongs to the instance itself.In impl blocks there is another Self in the scope which does not happen to traits.
I'll say upfront that I'm a Rust beginner, but the issue I'm discussing is exactly the kind of thing that experienced developers tend to avoid altogether. I suspect the reason it still exists in the language isn't that people don't know how to solve it, but simply that it doesn't occur frequently enough to annoy someone sufficiently to push for a language-level change. For me, as someone who's just learning the language, this problem has been nagging at me, and I feel I've spent enough time thinking it through and discussing it on the forum to arrive at a logical and straightforward solution.
My RFC proposes what I believe is the most intuitive and readable syntax for disambiguating method names that I can imagine. I think even without reading the full text, it's immediately clear what
obj.Self::method(args),obj.Category::method(args), andobj.(Library::Trait::method)(args)do. You can even guess why the parentheses are required aroundLibrary::Trait::methodbut not aroundobj.Self::method.I used an LLM to help turn the idea into a proper RFC, and now I have the persistent feeling that the list of edits I want to make isn't getting any shorter. It's hard to say I'd have done a better job myself, given my English proficiency and the fact that when you've been nursing an idea for long enough, no retelling — not even your own — ever feels complete. So it's possible I'm just overthinking it and can no longer distinguish real logical or presentation issues from a mere sense of incompleteness.
I'm really looking forward to your feedback so I can focus my revisions on what actually matters — things that are unclear to someone who isn't the author of RFC, or issues I've simply overlooked.
Rendered