Skip to content

Commit c56616d

Browse files
author
Syudoer
committed
Major Rewrite
1 parent b3ff088 commit c56616d

File tree

1 file changed

+131
-62
lines changed

1 file changed

+131
-62
lines changed

text/0000-obj-action-method-disambiguation.md

Lines changed: 131 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -8,83 +8,159 @@
88

99
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:
1010

11-
1. **Ad-hoc Disambiguation**: `expr.(Trait::method)(args)` allows invoking a specific trait method inline without breaking the method chain.
12-
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.
11+
1. **Trait Method Call**: `expr.(path::to::Trait::method)(args)` allows invoking a specific trait's method inline without breaking the method chain. (The parentheses around `path::to::Trait::method` are required)
12+
2. **inherent Method Call**: `expr.Self::method(args)` is an explicit way to call an inherent method. (Unlike the previous case parentheses are not allowed)
1313

1414
## Motivation
1515
[motivation]: #motivation
1616

17-
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.
17+
Currently, Rust's "Fully Qualified Syntax" (UFCS), e.g., `Trait::method(&obj)`, is the main mechanism to disambiguate method calls between inherent implementations and traits, or between multiple traits.
1818

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

23-
This creates significant friction:
24-
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.
25-
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.
26-
27-
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).
28-
2923
## Guide-level explanation
3024
[guide-level-explanation]: #guide-level-explanation
3125

32-
### The Problem: Ambiguous Methods
26+
There are three ways to have something callable inside of `obj`:
27+
- as a field containing a pointer to a function
28+
- as an inherent method
29+
- as a trait method
3330

34-
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`.
31+
While the first one is not confusing and has its unique syntax `(value.field)(args)`, the other two may cause some unexpected completely unrelated errors.
3532

33+
Imagine you have this piece of code
3634
```rust
37-
struct Image;
38-
impl Image {
39-
fn rotate(&self) { println!("Optimized internal rotation"); }
35+
use std::fmt::Display;
36+
37+
struct SomeThing<T> {
38+
something: fn(T),
39+
}
40+
41+
impl<T: Copy + Display> SomeThing<T> {
42+
fn something(&self, _arg: T) {
43+
println!("inherent fn called, got {}", _arg)
44+
}
45+
}
46+
47+
trait SomeTrait<T: Copy + Display> {
48+
fn something(&self, _arg: T);
4049
}
4150

42-
use graphic_lib::Transform;
43-
impl Transform for Image {
44-
fn rotate(&self) { println!("Generic geometric rotation"); }
45-
fn crop(&self) { ... }
51+
impl<T: Copy + Display> SomeTrait<T> for SomeThing<T> {
52+
fn something(&self, _arg: T) {
53+
println!("trait fn called, got {}", _arg);
54+
55+
print!("\t");
56+
self.something(_arg);
57+
}
58+
}
59+
60+
fn main() {
61+
let value = SomeThing { something: |_arg: i32| {println!("fn pointer called, got {_arg}")} };
62+
63+
value.something(1);
64+
(value.something)(2);
65+
SomeTrait::something(&value, 3);
4666
}
4767
```
4868

49-
Calling `img.rotate()` is now ambiguous or defaults to the inherent method when you might intend to use the trait implementation.
69+
it works, it handles all three ways and prints
70+
```plain
71+
inherent fn called, got 1
72+
fn pointer called, got 2
73+
trait fn called, got 3
74+
inherent fn called, got 3
75+
```
5076

51-
### Solution 1: Ad-hoc Disambiguation (The "Quick Fix")
77+
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
78+
79+
```plain
80+
inherent fn called, got 1
81+
fn pointer called, got 2
82+
trait fn called, got 3
83+
trait fn called, got 3
84+
trait fn called, got 3
85+
trait fn called, got 3
86+
trait fn called, got 3
87+
trait fn called, got 3
88+
trait fn called, got 3
89+
trait fn called, got 3
90+
trait fn called, got 3
91+
trait fn called, got 3
92+
trait fn called, got 3
93+
...
94+
```
5295

53-
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:
96+
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
97+
```rust
98+
impl<T: Copy + Display> SomeTrait<T> for SomeThing<T> {
99+
fn something(&self, _arg: T) {
100+
println!("trait fn called, got {}", _arg);
101+
102+
print!("\t");
103+
self.something(_arg); // here
104+
}
105+
}
106+
```
107+
108+
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)`.
54109

55110
```rust
56-
// Calls the Trait implementation while maintaining the chain
57-
img.crop().(Transform::rotate)().save();
111+
impl<T: Copy + Display> SomeTrait<T> for SomeThing<T> {
112+
fn something(&self, _arg: T) {
113+
println!("trait fn called, got {}", _arg);
114+
115+
print!("\t");
116+
self.Self::something(_arg);
117+
}
118+
}
58119
```
59120

60-
This tells the compiler: "Use the `rotate` method from the `Transform` trait on this object."
121+
`value.Self::method()` allows the compiler to only use an inherent method called `method` and errors if it hasn't been found.
61122

62-
**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.
123+
### Method Chain Conflicts
63124

64-
### Solution 2: Definition-site Aliases (The "API Design" Fix)
125+
Sometimes the ambiguity arises not within an implementation, but when using a type that implements traits with overlapping method names.
65126

66-
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.
127+
Consider a scenario where you have a `Builder` struct that implements both a `Reset` trait and has an inherent `reset` method.
67128

68129
```rust
69-
impl Image {
70-
// Inherent method
71-
fn rotate(&self) { ... }
130+
struct Builder;
131+
impl Builder {
132+
fn build(&self) -> String { "done".to_string() }
133+
fn reset(&self) -> &Self { self }
134+
}
135+
136+
trait Reset {
137+
fn reset(&self) -> &Self;
138+
}
139+
140+
impl Reset for Builder {
141+
fn reset(&self) -> &Self { self }
142+
}
72143

73-
// Expose the Transform trait as 'OtherOps' (Operations)
74-
pub use graphic_lib::Transform as OtherOps;
144+
fn main() {
145+
let b = Builder;
146+
// Defaults to the inherent method `reset` but silently falls back to the trait implementation if the inherent method is removed or renamed
147+
b.reset().build();
75148
}
76149
```
77150

78-
Now, users can access these methods via the alias, which is conceptually part of the `Image` type:
151+
Using the new syntax, you can explicitly choose which method to use without breaking the chain:
79152

80153
```rust
81-
let img = Image::new();
154+
fn main() {
155+
let b = Builder;
82156

83-
img.Self::rotate(); // Explicitly calls inherent (Optimized)
84-
img.OtherOps::rotate(); // Calls via Alias -> Transform (Generic)
85-
```
157+
// Use the inherent reset method
158+
b.Self::reset().build();
86159

87-
The `Self` keyword is implicitly treated as an alias for the inherent implementation, ensuring symmetry.
160+
// Use the trait's reset method explicitly
161+
b.(Reset::reset)().build();
162+
}
163+
```
88164

89165
## Reference-level explanation
90166
[reference-level-explanation]: #reference-level-explanation
@@ -94,54 +170,46 @@ The `Self` keyword is implicitly treated as an alias for the inherent implementa
94170
The `MethodCallExpr` grammar is extended in two specific ways:
95171

96172
1. **Parenthesized Path**: `Expr '.' '(' TypePath ')' '(' Args ')'`
97-
* This syntax is used for **Ad-hoc Disambiguation**.
173+
* This syntax is used for **Explicit Trait Method Calls**.
98174
* **Resolution**: The `TypePath` is resolved. If it resolves to a trait method, it is invoked with `Expr` as the receiver (the first argument).
99175
* **Desugaring**: `obj.(Path::method)(args)` desugars to `Path::method(obj, args)`, ensuring correct autoref/autoderef behavior for `obj`.
100176
* **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()`.
101177

102-
2. **Aliased Path**: `Expr '.' Ident '::' Ident '(' Args ')'`
103-
* This syntax is used for **Definition-site Aliases**.
104-
* **Resolution**: The first `Ident` is looked up in the `impl` block of the `Expr`'s type.
105-
* **Alias Matching**:
106-
* If `Ident` matches a `pub use Trait as Alias;` statement, the call resolves to `<Type as Trait>::method`.
107-
* The keyword `Self` is implicitly treated as an alias for the inherent implementation. `obj.Self::method()` resolves to the inherent method.
108-
109-
3. **Inherent Impl Items**:
110-
* A `use Trait as Alias;` item is now valid within an inherent `impl` block.
111-
* `use Trait;` is also supported as a shorthand for `use Trait as Trait;`.
112-
* Visibility modifiers (e.g., `pub`) are supported and determine the visibility of the alias for method resolution.
178+
2. **Explicit Inherent Path**: `Expr '.' 'Self' '::' Ident '(' Args ')'`
179+
* This syntax is used for **Explicit Inherent Method Calls**.
180+
* **Resolution**: The `Ident` is looked up strictly within the inherent implementation of `Expr`'s type.
181+
* **Semantics**: `obj.Self::method()` resolves to the inherent method `method`. It effectively bypasses trait method lookup.
113182

114183
### Resolution Logic Summary
115184

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

120-
* **Case: `obj.Alias::method(...)`**
121-
* Compiler looks up `Alias` in `obj`'s type definition.
122-
* If found, maps to corresponding Trait implementation.
189+
* **Case: `obj.Self::method(...)`**
190+
* Compiler looks up `method` in `obj`'s inherent implementation.
123191

124192
## Drawbacks
125193
[drawbacks]: #drawbacks
126194

127-
* **Cognitive Load** Increasing the cognitive load of users and Rust developers by adding more features. The definition-site aliases may eventually become a common way to group methods forcing users to deal with this feature not only in the case of method name disambiguation
128-
* **Parser Complexity**: The parser requires lookahead or distinct rules to distinguish `.` followed by `(` (method call) versus `.` followed by `Ident` followed by `::` (aliased call).
195+
* **Parser Complexity**: The parser requires lookahead or distinct rules to distinguish `.` followed by `(` (method call) versus `.` followed by `Self` followed by `::`.
129196
* **Punctuation Noise**: The syntax `.(...)` introduces more "Perl-like" punctuation symbols to the language, which some may find unaesthetic.
130197

131198
## Rationale and alternatives
132199
[rationale-and-alternatives]: #rationale-and-alternatives
133200

134-
* **Why Aliases?**
135-
* 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.
136-
* It mirrors C++ explicit qualification (e.g., `obj.Base::method()`).
137-
* **Why Parentheses for Ad-hoc?**
138-
* `obj.Trait::method` looks like there is something called `Trait` inside the `obj` while `Trait` is coming from the scope of the call
139-
* `obj.(Trait::method)` shows that `Trait::method` is evaluated first and then applied to the obj
201+
* **Why Parentheses for Trait Method Calls?**
202+
* `value.Trait::method` looks like there is something called `Trait` inside the `value` while `Trait` is coming from the scope of the call.
203+
* `value.(Trait::method)` shows that `Trait::method` is evaluated first and then applied to the `value`.
204+
* **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 for ad-hoc trait paths avoids closing the door on this design space.
205+
* **Why No Parentheses for Inherent Method Call?**
206+
* Unlike `Trait`, which comes from the outer scope, `Self` conceptually belongs to the instance itself.
207+
* `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.
140208

141209
## Prior art
142210
[prior-art]: #prior-art
143211

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

146214
## Unresolved questions
147215
[unresolved-questions]: #unresolved-questions
@@ -152,3 +220,4 @@ The `MethodCallExpr` grammar is extended in two specific ways:
152220
[future-possibilities]: #future-possibilities
153221

154222
* **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.
223+
* **Disabling Inherent Preference**: A specialized macro or attribute could be introduced to opt-out of the default "inherent-first" resolution rule (effectively canceling the implicit `use Self for *`). This aligns with Rust's philosophy of explicit over implicit behavior where ambiguity exists, ensuring that code correctness is verifiable.

0 commit comments

Comments
 (0)