Skip to content
154 changes: 154 additions & 0 deletions text/0000-obj-action-method-disambiguation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
- Feature Name: `obj-action-method-disambiguation`
- Start Date: 2026-01-20
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
Comment thread
syudoer marked this conversation as resolved.
Outdated
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

## Summary
[summary]: #summary

This RFC proposes two extensions to Rust's method call syntax to unify method resolution and maintain fluent method chaining ("noun-verb" style) in the presence of naming ambiguities:

1. **Ad-hoc Disambiguation**: `expr.(Trait::method)(args)` allows invoking a specific trait method inline without breaking the method chain.
2. **Definition-site Aliases**: `pub use Trait as Alias;` within `impl` blocks enables `expr.Alias::method(args)`, allowing type authors to expose traits as named "facets" of their API.

## Motivation
[motivation]: #motivation

Currently, Rust's "Fully Qualified Syntax" (UFCS), e.g., `Trait::method(&obj)`, is the main mechanism to resolve method name conflicts between inherent implementations and traits, or between multiple traits.

While robust, UFCS forces a reversal of the visual data flow, breaking the fluent interface pattern:
* **Fluent (Ideal)**: `object.process().output()`
* **Broken (Current)**: `Trait::output(&object.process())`

This creates significant friction:
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
Copy Markdown
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
Copy Markdown
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.


We propose a solution that restores the `object.action()` left-to-right flow in all cases and provides tools for both API consumers (ad-hoc fixes) and API producers (aliases).

## Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

### The Problem: Ambiguous Methods

Imagine you are using a type `Image` that has an optimized inherent `rotate` method. Later, you import a graphics library with a `Transform` trait that also defines `rotate`.

```rust
struct Image;
impl Image {
fn rotate(&self) { println!("Optimized internal rotation"); }
}

use graphic_lib::Transform;
impl Transform for Image {
fn rotate(&self) { println!("Generic geometric rotation"); }
fn crop(&self) { ... }
}
```

Calling `img.rotate()` is now ambiguous or defaults to the inherent method when you might intend to use the trait implementation.

### Solution 1: Ad-hoc Disambiguation (The "Quick Fix")

If you are a consumer of this API and need to resolve this ambiguity immediately without breaking your method chain, you can use parentheses to specify the trait:

```rust
// Calls the Trait implementation while maintaining the chain
img.crop().(Transform::rotate)().save();
```

This tells the compiler: "Use the `rotate` method from the `Transform` trait on this object."

**Note on `Self`**: While `img.(Self::rotate)()` is grammatically possible in some cases, it is discouraged. The compiler will warn you to remove the parentheses and use the explicit alias syntax described below.

### Solution 2: Definition-site Aliases (The "API Design" Fix)

As the author of `Image`, you can prevent this friction for your users. Instead of forcing them to import `graphic_lib::Transform` to access specific functionality, you can expose that trait as a named part of your `Image` API.

```rust
impl Image {
// Inherent method
fn rotate(&self) { ... }

// Expose the Transform trait as 'OtherOps' (Operations)
pub use graphic_lib::Transform as OtherOps;
}
```

Now, users can access these methods via the alias, which is conceptually part of the `Image` type:

```rust
let img = Image::new();

img.Self::rotate(); // Explicitly calls inherent (Optimized)
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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)
}


## Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

### Grammar Extensions

The `MethodCallExpr` grammar is extended in two specific ways:

1. **Parenthesized Path**: `Expr '.' '(' TypePath ')' '(' Args ')'`
* This syntax is used for **Ad-hoc Disambiguation**.
* **Resolution**: The `TypePath` is resolved. If it resolves to a trait method, it is invoked with `Expr` as the receiver (the first argument).
* **Desugaring**: `obj.(Path::method)(args)` desugars to `Path::method(obj, args)`, ensuring correct autoref/autoderef behavior for `obj`.
* **Restriction**: Using `(Self::method)` inside an `impl` block where `Self` is a type alias triggers a compiler warning, suggesting the removal of parentheses and usage of `Expr.Self::method()`.

2. **Aliased Path**: `Expr '.' Ident '::' Ident '(' Args ')'`
* This syntax is used for **Definition-site Aliases**.
* **Resolution**: The first `Ident` is looked up in the `impl` block of the `Expr`'s type.
* **Alias Matching**:
* 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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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()

* A `use Trait as Alias;` item is now valid within an inherent `impl` block.
* `use Trait;` is also supported as a shorthand for `use Trait as Trait;`.
* Visibility modifiers (e.g., `pub`) are supported and determine the visibility of the alias for method resolution.

### Resolution Logic Summary

* **Case: `obj.(Trait::method)(...)`**
* Compiler verifies `Trait` is in scope or fully qualified.
* Resolves to UFCS call with `obj` as first arg.

* **Case: `obj.Alias::method(...)`**
* Compiler looks up `Alias` in `obj`'s type definition.
* If found, maps to corresponding Trait implementation.

## Drawbacks
[drawbacks]: #drawbacks

* **Cognitive Load** Increasing the cognitive load of users and Rust developers by adding more features
* **Parser Complexity**: The parser requires lookahead or distinct rules to distinguish `.` followed by `(` (method call) versus `.` followed by `Ident` followed by `::` (aliased call).
* **Punctuation Noise**: The syntax `.(...)` introduces more "Perl-like" punctuation symbols to the language, which some may find unaesthetic.

## Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

* **Why Aliases?**
* The primary benefit is for the **consumer**. They should not need to know the origin module of a trait to use it. Aliasing bundles the dependency with the type, treating the trait as a named interface/facet of the object.
* 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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 thread
workingjubilee marked this conversation as resolved.
Outdated

## Prior art
[prior-art]: #prior-art

* **C++**: Allows explicit qualification of method calls using `obj.Base::method()`, which served as inspiration for the Aliased Path syntax.

## Unresolved questions
[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]()`).

## Future possibilities
[future-possibilities]: #future-possibilities

* **Scoped Prioritization**: We can also introduce syntax like `use Trait for Foo` or `use Self for Foo` within a function scope to change default resolution without changing call sites.