-
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
Changes from 4 commits
5e4af1f
416ca38
983363d
e50c1ab
a7f630b
b3ff088
c56616d
2ab02f5
012817b
d2f698b
70322f1
d5159b2
3786188
86bd195
5c13f85
0867025
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,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) | ||
| - 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. | ||
|
Member
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. 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 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.
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 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:
|
||
|
|
||
| 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. | ||
|
Member
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 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()
}
}
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. Do you think there is a need for Probably there is a better name for the alias
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.
It would be useful when you implement your own
Member
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. currently in order for you to call a trait method the trait has to be in scope either via 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 so I don't think
Member
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. 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 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**: | ||
|
Member
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. imo a
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. Isn't that the same? or you mean
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've finally understood. Yeah, I also think so.
Member
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. (edit: didn't see your comment until after I posted this) 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!()
}
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. Imo
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. But there is no reason to restrict this
Member
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. yeah, it could be useful for when you're writing some proc-macro that needs to access traits based off of syntax like |
||
| * 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. | ||
|
Member
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 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.
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'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.
Member
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. 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.
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. | ||
Uh oh!
There was an error while loading. Please reload this page.