-
Notifications
You must be signed in to change notification settings - Fork 1.7k
RFC: Natural Method Disambiguation #3913
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
base: master
Are you sure you want to change the base?
Changes from all commits
c00697e
01f6f2c
e0f99d4
f533794
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,295 @@ | ||
| - Feature Name: `natural-method-disambiguation` | ||
| - Start Date: 2026-01-27 | ||
| - RFC PR: [rust-lang/rfcs#3913](https://github.com/rust-lang/rfcs/pull/3913) | ||
| - Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) | ||
|
|
||
| ## Summary | ||
| [summary]: #summary | ||
|
|
||
| The proposal introduces two new forms of method call syntax for method name disambiguation that keep the receiver on the left and preserve chaining. | ||
|
|
||
| 1. **Trait Method Call**: `expr.<path::to::Trait>::method(args)` allows invoking a specific trait's method inline without breaking the method chain. | ||
| 2. **Inherent Method Call**: `expr.Self::method(args)` is an explicit way to call an inherent method. | ||
|
|
||
| ## Motivation | ||
| [motivation]: #motivation | ||
|
|
||
| ### Method chain break | ||
| Currently, Rust's "Fully Qualified Syntax" (UFCS), e.g., `<Type as Trait>::method(&obj)` (or less commonly `Trait::method(&obj)`), is the main mechanism to disambiguate method calls between inherent implementations and traits, or between multiple traits. | ||
|
|
||
| It is worth noting that the proposed syntax is essentially a minor reordering that shortens the construct by removing `Type as` and the `&`/`&mut` operators, which carry no specific disambiguation information in this context. | ||
|
|
||
| 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())` | ||
|
|
||
| ### Silent bugs and Fragility | ||
|
|
||
| Currently, Rust's method resolution follows a fixed priority: it defaults to an inherent method if one exists. If no inherent method is found, the compiler looks for traits in scope that provide the method. If exactly one such trait is implemented for the type, the compiler selects it; otherwise, it returns an error. | ||
|
|
||
| This creates a "Primary and Fallback" mechanism where the compiler can silently switch between logic. If a primary (inherent) method is removed or renamed, the compiler may silently fall back to a trait implementation. Conversely, adding an inherent method can unexpectedly shadow an existing trait method call. | ||
|
|
||
| In rare cases, modifying one part of the code can unexpectedly alter logic elsewhere, causing a chain reaction of errors that makes it difficult to locate the root cause. | ||
|
|
||
| ### Summary | ||
|
|
||
| This RFC aims to fully solve the problem of fluent method chaining. The second problem (fragility) requires a more complex approach, with this RFC being the first step. More details on potential future solutions are discussed in the [Future Possibilities](#future-possibilities) section. | ||
|
|
||
| ## Guide-level explanation | ||
| [guide-level-explanation]: #guide-level-explanation | ||
|
|
||
| There are three ways to have something callable on `obj`: | ||
| - as a field containing a pointer to a function | ||
| - as an inherent method | ||
| - as a trait method | ||
|
|
||
| While the first one is not confusing and has its unique syntax `(value.field)(args)`, the other two may cause some unexpected errors that seem unrelated to the actual mistake. | ||
|
|
||
| Imagine you have this piece of code | ||
| ```rust | ||
| use std::fmt::Display; | ||
|
|
||
| struct SomeThing<T> { | ||
| something: fn(T), | ||
| } | ||
|
|
||
| impl<T: Copy + Display> SomeThing<T> { | ||
| fn something(&self, _arg: T) { | ||
| println!("inherent fn called, got {}", _arg) | ||
| } | ||
| } | ||
|
|
||
| trait SomeTrait<T: Copy + Display> { | ||
| fn something(&self, _arg: T); | ||
| } | ||
|
|
||
| impl<T: Copy + Display> SomeTrait<T> for SomeThing<T> { | ||
| fn something(&self, _arg: T) { | ||
| println!("trait fn called, got {}", _arg); | ||
|
|
||
| print!("\t"); | ||
| self.something(_arg); | ||
| } | ||
| } | ||
|
|
||
| fn main() { | ||
| let value = SomeThing { something: |_arg: i32| {println!("fn pointer called, got {}", _arg)} }; | ||
|
|
||
| value.something(1); | ||
| (value.something)(2); | ||
| SomeTrait::something(&value, 3); // So that this can be compiled and checked in the current version of Rust | ||
| } | ||
| ``` | ||
|
|
||
| it works, it handles all three ways and prints | ||
| ```plain | ||
| inherent fn called, got 1 | ||
| fn pointer called, got 2 | ||
| trait fn called, got 3 | ||
| inherent fn called, got 3 | ||
| ``` | ||
|
|
||
| but if you change the line `impl<T: Copy + Display> SomeTrait<T> for SomeThing<T> {` to `impl<T: Copy + Display, U: Copy> SomeTrait<T> for SomeThing<U> {` instead of producing an error for the mismatch, the code compiles successfully and prints | ||
|
|
||
| ```plain | ||
| inherent fn called, got 1 | ||
| fn pointer called, got 2 | ||
| trait fn called, got 3 | ||
| trait fn called, got 3 | ||
| trait fn called, got 3 | ||
| trait fn called, got 3 | ||
| trait fn called, got 3 | ||
| trait fn called, got 3 | ||
| trait fn called, got 3 | ||
| trait fn called, got 3 | ||
| trait fn called, got 3 | ||
| trait fn called, got 3 | ||
| ... | ||
| ``` | ||
|
|
||
| > [!NOTE] | ||
| > Since `U: Copy` lacks `+ Display` bound required by the inherent implementation, the inherent method is not applicable within this context, causing the compiler to resolve to the trait method silently. | ||
|
|
||
| You would also get the same undesirable behavior in another case. You could rename `something` in `SomeThing`'s impl block and forget to rename it in the `SomeTrait`'s impl block | ||
| ```rust | ||
| impl<T: Copy + Display> SomeTrait<T> for SomeThing<T> { | ||
| fn something(&self, _arg: T) { | ||
| println!("trait fn called, got {}", _arg); | ||
|
|
||
| print!("\t"); | ||
| self.something(_arg); // here | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| To prevent this and ensure the compiler rejects broken code, it would be better to use `self.Self::something(_arg)` instead of `self.something(_arg)`. | ||
|
|
||
| ```rust | ||
| impl<T: Copy + Display> SomeTrait<T> for SomeThing<T> { | ||
| fn something(&self, _arg: T) { | ||
| println!("trait fn called, got {}", _arg); | ||
|
|
||
| print!("\t"); | ||
| self.Self::something(_arg); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| `value.Self::method()` allows the compiler to only use an inherent method called `method` and errors if it hasn't been found. | ||
|
|
||
| ### Method Chain Conflicts | ||
|
|
||
| Sometimes the ambiguity arises not within an implementation, but when using a type that implements traits with overlapping method names. | ||
|
|
||
| Consider a scenario where you have a `Builder` struct that implements both a `Reset` trait and has an inherent `reset` method. | ||
|
|
||
| ```rust | ||
| struct Builder; | ||
| impl Builder { | ||
| fn build(&self) -> String { "done".to_string() } | ||
| fn reset(&self) -> &Self { self } | ||
| } | ||
|
|
||
| trait Reset { | ||
| fn reset(&self) -> &Self; | ||
| } | ||
|
|
||
| impl Reset for Builder { | ||
| fn reset(&self) -> &Self { self } | ||
| } | ||
|
|
||
| fn main() { | ||
| let b = Builder {}; | ||
| // Defaults to the inherent method `reset` but silently falls back to the trait implementation if the inherent method is removed or renamed | ||
| b.reset().build(); | ||
| } | ||
| ``` | ||
|
|
||
| Using the explicit qualification syntax, you can explicitly choose which method to use without breaking the chain: | ||
|
|
||
| ```rust | ||
| fn main() { | ||
| let b = Builder; | ||
|
|
||
| // Use the inherent reset method | ||
| b.Self::reset().build(); | ||
|
|
||
| // Use the trait's reset method explicitly | ||
| b.<Reset>::reset().build(); | ||
| } | ||
| ``` | ||
|
|
||
| The `obj.<path::to::Trait>::method()` syntax allows disambiguating calls to trait methods. | ||
|
|
||
| This syntax is frequently used for disambiguation between different traits on the same object. Consider a more complex example from a simulation game where you have a `HydrocarbonDeposit` type. This type might implement multiple traits representing different resource extraction methods, such as `resources::sources::HeavyOilSource`, `resources::sources::LightOilSource`, and `resources::sources::NaturalGasSource`. | ||
|
|
||
| Each of these traits might have a `simulate` method that is not in-place (i.e., it returns a simulated version of the object rather than modifying it). | ||
|
|
||
| ```rust | ||
| use resources; | ||
|
|
||
| fn process_deposit(deposit: HydrocarbonDeposit) { | ||
| // Run simulation specifically for heavy oil extraction behavior | ||
| let simulated = deposit | ||
| .<HeavyOilSource>::simulate() | ||
| .<LightOilSource>::simulate() | ||
| .<NaturalGasSource>::simulate(); | ||
| } | ||
| ``` | ||
|
|
||
| ## Reference-level explanation | ||
| [reference-level-explanation]: #reference-level-explanation | ||
|
|
||
| ### Grammar Extensions | ||
|
|
||
| The `MethodCallExpr` grammar is extended in two specific ways: | ||
|
|
||
| 1. **Angle Bracketed Path**: `Expr '.' '<' TypePath '>' '::' Ident '(' Args ')'` | ||
| * This syntax is used for **Explicit Trait Method Calls**. | ||
| * **Resolution**: The `TypePath` is resolved. If it resolves to a trait, the `Ident` method from that trait is invoked with `Expr` as the receiver (the first argument). | ||
| * **Desugaring**: `obj.<Path>::method(args)` desugars to `<Type as Path>::method(obj, args)`, ensuring correct autoref/autoderef behavior for `obj`. | ||
| * **Restriction**: `Expr.<Self>::method(Args)` is not allowed (use `Expr.Self::method` instead). | ||
|
|
||
| 2. **Explicit Inherent Path**: `Expr '.' 'Self' '::' Ident '(' Args ')'` | ||
| * This syntax is used for **Explicit Inherent Method Calls**. | ||
| * **Resolution**: The `Ident` is looked up strictly within the inherent implementation of `Expr`'s type. | ||
| * **Semantics**: `obj.Self::method()` resolves to the inherent method `method` (equivalent to `<Type>::method` or `<Type as Type>::method`). It effectively bypasses trait method lookup. | ||
|
|
||
| ### Resolution Logic Summary | ||
|
|
||
| * **Case: `obj.<Trait>::method(...)`** | ||
| * Resolves to `<Type as Trait>::method(obj, args)`. | ||
|
|
||
| * **Case: `obj.Self::method(...)`** | ||
| * Resolves to `<Type>::method(obj, args)`. | ||
|
|
||
| ## Drawbacks | ||
| [drawbacks]: #drawbacks | ||
|
|
||
| * **Parser Complexity**: The parser requires distinct rules to distinguish `.` followed by `<` (explicit trait call) versus `.` followed by `Self`. | ||
| * **Visual Noise**: The syntax `.<...>::` adds complexity to method chains. | ||
| * **Inconsistency**: It may confuse some users that `Self` does not require brackets while traits do. | ||
|
|
||
| ## Rationale and alternatives | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the main alternative is a pipeline operator: foo
|> Bar::bar()
|> baz()The advantage of this RFC is that it doesn't require a new operator. The advantage of a pipeline operator is that it can be used with functions that aren't methods (i.e. functions without a |
||
| [rationale-and-alternatives]: #rationale-and-alternatives | ||
|
|
||
| * **Why Angle Brackets 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. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I disagree. The parser can tell that in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be more natural to allow anything referring to a type or trait after the dot: recv.Foo::bar() // desugars to:
Foo::bar(recv)
recv.path::to::Foo::<i32>::bar() // desugars to
path::to::Foo::<i32>::bar(recv)
recv.<Foo as Baz>::bar() // desugars to
<Foo as Baz>::bar(recv)
recv.<Foo>::bar() // desugars to
<Foo>::bar(recv)
recv.Self::bar() // special casedExcept that the method call syntax implicitly inserts Writing the type (not just the trait) of the receiver can be useful, for example to aid type inference: foo.into()
.<Bar as Baz>::baz() // compiles
foo.into()
.Baz::baz() // error: type annotations needed |
||
| * `value.<Trait>::method` aligns with Rust's existing use of angle brackets for type-related disambiguation (like UFCS `<Type as Trait>::method`). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it worth explicitly listing an extended UFCS syntax as an alternative? For example:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would you add extra characters that don't add any information?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not saying it's a better alternative to what's currently in the RFC. It might be worth listing them to show they have been considered. (And to cover future questions/suggestions like this.) One possible reason is consistency, but I don't think it's a good enough reason to justify the extra characters in this specific situation.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I can just refer to this in such cases |
||
| * **Reservation**: We specifically reserve the unbracketed syntax `value.Category::method()` (where `Category` is not `Self`) for possible future language features, such as "Categorical" or "Facet" views of an object. | ||
|
|
||
| * **Why No Parentheses for Inherent Method Call?** | ||
|
|
||
| * The construct `<path::to::Trait>` has a consistent, independent meaning (the trait itself) regardless of the object it is applied to, which the angle brackets appropriately denote. Conversely, `<Self>` without the `obj.` prefix is context-dependent: it might refer to the type of `obj`, a different type entirely (e.g., the `Self` of the surrounding impl block), or nothing at all. Therefore, it is semantically preferable to associate `Self` more strongly with the object instance using the `obj.Self` syntax, effectively treating it as a pseudo-member access | ||
|
|
||
| * In `impl` blocks, we can apply `obj.Self` to objects that do not have the type named `Self` in that block. `obj.<Self>` would look like we are trying to apply a method of one type to an object of another type even if they happen to be the same. | ||
|
|
||
| * Despite being technically feasible for the compiler to parse, `obj.<Self>` would appear clunky and unidiomatic. | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another alternative or future possibility is:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The second is a good one |
||
| ## Prior art | ||
| [prior-art]: #prior-art | ||
|
|
||
| ## Prior art | ||
|
|
||
| Several programming languages face similar challenges with method disambiguation when inherent implementations conflict with trait-/interface-/extension-provided methods. Approaches generally fall into three categories: explicit qualification syntax, cast-based selection, or strict implicit resolution (sometimes with anti-features). | ||
|
|
||
| - **C++** | ||
| C++ supports direct qualification with the scope resolution operator: `obj.Base::method()` or `obj.Trait::method()`. | ||
| This is the closest analogue to the proposed `obj.Self::method()` (for inherent methods) and `obj.<Trait>::method()` (for trait methods). | ||
| It preserves chaining ergonomics and readability, which is a key inspiration for this RFC. | ||
|
|
||
| - **C#** | ||
| When a type explicitly implements interface members or when multiple interfaces provide the same method, disambiguation is typically achieved via explicit casts: `((IInterface)obj).Method()`. | ||
| Extension methods (somewhat analogous to blanket impls over traits) are resolved statically and can be called explicitly via the static class if needed, but there is no dedicated qualification syntax for instance calls. | ||
| Cast-based approaches interrupt method chaining and reduce readability compared to qualification. | ||
|
|
||
| - **Java** | ||
| Similar to C#, external disambiguation requires casts: `((Interface)obj).method()`. | ||
| Inside a class implementing multiple interfaces with default methods, one can use qualified super calls: `Interface.super.method()`. | ||
| Again, external calls rely on casts, which do not chain naturally. | ||
|
|
||
| - **Kotlin** | ||
| Within a class, qualified super calls are supported: `super<Interface>.method()`. | ||
| For external calls on an object, disambiguation uses casts: `(obj as Interface).method()`. | ||
| The internal syntax is close to the proposal, but external calls suffer the same chaining issues as cast-based approaches. | ||
|
|
||
| - **Swift** (anti-example) | ||
| Swift deliberately prohibits any form of type qualification or annotation on method calls to keep the grammar simple. | ||
| This can lead to ambiguities in generic code that require workarounds, such as passing explicit type information through parameters or using separate overloads. | ||
| This demonstrates the pitfalls of making disambiguation impossible when it is occasionally needed. | ||
|
|
||
| Many other languages (e.g., Haskell, Go) rely entirely on implicit resolution via type-class/instance selection or interface satisfaction, with coherence rules preventing most ambiguities. When ambiguities do arise, they are usually treated as errors requiring code restructuring rather than providing a syntactic escape hatch. | ||
|
|
||
| The current Rust approach (`<Type as Trait>::method(&mut obj)` and `Trait::method(&mut obj)`) works but is verbose and it breaks natural chaining. This proposal builds on C++-style qualification while adapting it to Rust’s orphan and coherence rules, offering explicit control without sacrificing ergonomics. | ||
|
|
||
| ## Unresolved questions | ||
| [unresolved-questions]: #unresolved-questions | ||
|
|
||
| *None* | ||
|
|
||
| ## 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. | ||
| * **Disabling Inherent Preference**: A specialized macro or attribute could be introduced to opt-out of the default "inherent-first" resolution rule. | ||
| * **Warning on Signature Collision**: Since identical signatures (matching positional argument types) are rare, we could warn on such overlaps. This would flag fragile call sites and detect when a newly added inherent method silently hijacks existing trait-based invocations. | ||
Uh oh!
There was an error while loading. Please reload this page.
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 assume this code causes infinite stack recursion at runtime?
There's already a compiler check for simple infinite recursion. But it's hard to lint against more complex cases of silent receiver changes.
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 same undesirable behavior" already refers to infinite recursion at runtime which I showed above. If it still doesn't seem redundant to you even keeping that in mind, then okay, I'll go ahead and commit 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.
It might be worth specifically saying that the undesirable behaviour is only detected at runtime, and that it's an infinite recursion terminating in a stack overflow. Silent errors like this are a stronger justification for language changes.
They're implied by the example code and outputs, but a busy/casual reader might miss it.
As you say, it probably belongs further up, near "the code compiles successfully and prints"
Uh oh!
There was an error while loading. Please reload this page.
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.
Note
Since
U: Copylacks+ Displaybound required by the inherent implementation, the inherent method is not applicable within this context, causing the compiler to resolve to the trait method silently which results in infinite recursion at runtime.You would also get infinite recursion in another case. You could rename
somethinginSomeThing's impl block and forget to rename it in theSomeTrait's impl blockBetter?