Skip to content

Conversation

@syudoer
Copy link

@syudoer syudoer commented Jan 20, 2026

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), and obj.(Library::Trait::method)(args) do. You can even guess why the parentheses are required around Library::Trait::method but not around obj.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

@syudoer syudoer changed the title obj-action-method-disambiguation rfc commit RFC: obj-action style method disambiguation Jan 20, 2026
img.OtherOps::rotate(); // Calls via Alias -> Transform (Generic)
```

The `Self` keyword is implicitly treated as an alias for the inherent implementation, ensuring symmetry.
Copy link
Member

@programmerjake programmerjake Jan 20, 2026

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()
    }
}

Copy link
Author

@syudoer syudoer Jan 21, 2026

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

Copy link
Author

@syudoer syudoer Jan 21, 2026

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

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

Copy link
Member

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

Copy link
Member

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**:
Copy link
Member

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.

Copy link
Author

@syudoer syudoer Jan 20, 2026

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?

Copy link
Author

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.

Copy link
Member

@programmerjake programmerjake Jan 20, 2026

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!()
}

Copy link
Author

@syudoer syudoer Jan 20, 2026

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.

Copy link
Author

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

Copy link
Member

@programmerjake programmerjake Jan 20, 2026

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()

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label Jan 20, 2026
* 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.
Copy link
Member

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.

Copy link
Author

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.

Copy link
Member

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.

Comment on lines 24 to 25
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.
Copy link
Member

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.

Copy link
Author

@syudoer syudoer Jan 21, 2026

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.

@syudoer
Copy link
Author

syudoer commented Jan 21, 2026

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.

@syudoer
Copy link
Author

syudoer commented Jan 21, 2026

I'll do it tomorrow, it's too late for me

I am gonna leave only value.Self::method() for an explicit inherent method calling and value.(Trait::method)() for disambiguation.
The main reason for the parentheses will be reserving syntax value.Category::method() for possible feature features.

I started writing this example but the compiler appeared to be too smart.
https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=e9d7541e8fefb293943333a3ee867683
You may play with it while I'am offline

@syudoer
Copy link
Author

syudoer commented Jan 21, 2026

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.

@workingjubilee
Copy link
Member

workingjubilee commented Jan 21, 2026

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 <Type as Trait>::function allows you to call a trait impl directly, and will fail if that trait doesn't implement that function (with many caveats about what I mean by "that trait").

Using <Type>::function resolves in a similar way to method syntax.

When dealing with a generic T (or U or whatever) we only know the type implements a trait if it has bounds on the generic informing us those traits are relevant, so we cannot call the type's inherent impl. I mean, we could first try to monomorphize it and see, but for various reasons we do not want to do that, because it can result in notoriously bad errors. A big reason people find Rust palatable despite its learning curve is because we've deliberately tried to make choices, when the tradeoffs are right, to make it easier to generate nice errors.

@burdges
Copy link

burdges commented Jan 21, 2026

We already have syntax for accessing only inherent methods:

mod no_traits {
    pub fn do_whatever(...) { ... }
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=5129eabf9108e7952b0658bd9d4c98c9

@syudoer
Copy link
Author

syudoer commented Jan 22, 2026

I've rewritten the RFC. I hope it's cleaner this time

@syudoer
Copy link
Author

syudoer commented Jan 23, 2026

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]()`)?

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

Copy link
Author

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.

Copy link
Author

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

Copy link

@jdahlstrom jdahlstrom Jan 23, 2026

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, but obj.<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 like obj.<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.

@syudoer
Copy link
Author

syudoer commented Jan 23, 2026

Are @programmerjake and @workingjubilee going to do a second review? Just so I know what to expect

@programmerjake
Copy link
Member

programmerjake commented Jan 23, 2026

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.

Comment on lines +216 to +219
* **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.
Copy link
Member

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

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

Copy link
Author

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?

Copy link
Author

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.

Copy link
Member

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.

Copy link
Author

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

@tmccombs tmccombs Jan 24, 2026

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::reset

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

Copy link
Author

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.

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.

Copy link
Author

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.

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.

Copy link
Author

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.

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

Labels

T-lang Relevant to the language team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants