RFC: Natural Method Disambiguation#3913
Conversation
|
This is a direct follow-up to #3908, with a few key differences:
Therefore, I have decided to start with a clean slate. |
|
Afaik we cannot justify two syntaxes for UFC, and the proposed new syntax does not improve over the existing UFC for traits, but if we need clear access to inherent methods then roughly |
why not? we already have multiple different syntaxes for function calls:
I disagree. It's much more ergonomic to read and write when you have a method chain since you don't have to deal with all the nested parenthesis and reading the functions from the inside out.
I think we also want some syntax for only accessing inherent methods when you're not in a method chain, e.g. because your functions don't have a |
|
If you don't mind both of you, could you start an inline comment thread for this point?
|
I guess we should ping @burdges for this. |
teor2345
left a comment
There was a problem hiding this comment.
Some suggestions, mainly alternatives and prior art
|
|
||
| * **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. | ||
| * `value.<Trait>::method` aligns with Rust's existing use of angle brackets for type-related disambiguation (like UFCS `<Type as Trait>::method`). |
There was a problem hiding this comment.
Is it worth explicitly listing an extended UFCS syntax as an alternative?
For example:
value.<Type as Type>::methodvalue.<Type as Self>::methodvalue.<Self as NotATrait>::method(if chosen, we can bikeshed the spelling ofNotATraitlater) as an alternative tovalue.Self::methodorvalue.<Self>::method
There was a problem hiding this comment.
Why would you add extra characters that don't add any information?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Motivation
Method chain break
Currently, Rust's "Fully Qualified Syntax" (UFCS), e.g.,
<Type as Trait>::method(&obj)(or less commonlyTrait::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 asand the&/&mutoperators, which carry no specific disambiguation information in this context.
I can just refer to this in such cases
| ## Prior art | ||
| [prior-art]: #prior-art | ||
|
|
||
| * **C++**: Allows explicit qualification of method calls using `obj.Base::method()`. |
There was a problem hiding this comment.
Swift has anti-disambiguation prior art, but it still might be worth mentioning.
Swift bans any type annotations on method calls:
Which causes unfortunate consequences in some cases, like having to pass a type as a function parameter:
C# (and I think also C++) can disambiguate interfaces using type casts: ((Base)(obj)).method()
C# extensions are more similar to Rust traits, but I couldn't find specific documentation about disambiguating them.
| * 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. | ||
|
|
There was a problem hiding this comment.
Another alternative or future possibility is:
- not importing the trait, so you can't accidentally call its methods
- add a clippy lint or compiler warning for trait methods that will be called if the inherent method is renamed or removed, to help clean up trait imports
There was a problem hiding this comment.
The second is a good one
| > [!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 |
There was a problem hiding this comment.
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.
| 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 | |
| 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, causing infinite recursion at runtime |
There was a problem hiding this comment.
"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.
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"
There was a problem hiding this comment.
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 which results in infinite recursion at runtime.
You would also get infinite recursion in another case. You could rename something in SomeThing's impl block and forget to rename it in the SomeTrait's impl block
Better?
|
currently as a free function let a: u32 = Default::default(); // ok, we get `<u32 as Default>::default()`
let b: u32 = <Default>::default(); // error, interpreted as `<dyn Default>::default()`so I find it very strange the proposed "natural" syntax to be |
Using this, you would always have to write 5 extra characters: |
| [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.
I disagree. The parser can tell that in value.Foo::bar, Foo doesn't refer to a member or method because these are never followed by ::. And a human will immediately recognize it as a trait or type because it is uppercase.
There was a problem hiding this comment.
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 & or &mut when needed.
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 neededThere was a problem hiding this comment.
The parser can tell that in value.Foo::bar, Foo doesn't refer to a member or method because these are never followed by ::.
Only if the trait is a single token. If it is a path, like foo::Foo then value.foo::Foo looks like you are using the foo trait on value.foo. and may be ambiguous if value has a foo field.
There was a problem hiding this comment.
And a human will immediately recognize it as a trait or type because it is uppercase.
As far as I've seen, expressions in Rust can be read from left to right, word by word, token by token, but to recognize that the HeavyOilSource in deposit.HeavyOilSource::simulate() is a trait, one would have to read two tokens (simlutanously?), the HeavyOilSource and the ::, to recognize that HeavyOilSource is in a position of a trait.
Things get more complicated when you have a trait and a type of the same name (?? I haven't tried this before, only heard of it somewhere, not sure).
| * **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.
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 self argument).
There was a problem hiding this comment.
That opens up the question of what that looks like when there are multiple arguments. Can you use it to pass an argument that isn't the first argument?
There was a problem hiding this comment.
Perhaps something like:
foo |> Bar::bar(_, 1, 2)?
There was a problem hiding this comment.
Of note, . is already a pipeline operator, except that symbol lookup is scoped to associated symbols. Another option that has been mentioned elsewhere but not yet in the discussion of this RFC is to lean into that, and make foo . ($expr) () take $expr evaluating to any function item and call it using the method conventions (i.e. self as first argument, autoref, etc).
There was a problem hiding this comment.
foo . ($expr) ()
nice, if that works then I'd be happy with that and <Foo as Self>::foo syntax for the inherent associated function and then allow combining them like foo.(<_ as Self>::foo)()
| 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. |
There was a problem hiding this comment.
Why not make it consistent with the expr.<path::to::Trait>::method(args) syntax (expr.<Self>::method(args))? Self is a keyword in Rust, so there's no ambiguity. On the other hand, expr.Self::method(args) looks so close to expr::Self::method(args), expr.Self.method(args) etc. (and gives me the instinctive feeling that Self is a field of expr, requiring looking ahead to the :: to disambiguate)
There was a problem hiding this comment.
Just saw the note at R247
Will leave comment there.
|
|
||
| * 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. |
There was a problem hiding this comment.
Disagree. obj.<Self>.method() and obj.<Trait>.method() share the common form obj.<XXX>.method(). To me it's consistent instead of clunky.
As for not being idiomatic, I simply don't have that feeling, to be honest. (On the other hand, it will appear idiomatic once it is used often enough by the community :))
Also I don't know if it's a mistake, but obj.<Self> is used here, where only obj.<Self>.method() is meaningful. (Which is important because people may make mistakes like a.<Trait> + b.<Trait> which is meaningless because adding just uses the Add trait.)
There was a problem hiding this comment.
Was the syntax expr.<Trait>.method(args) considered? I acccidentally typoed it in my last comment and it looks not bad, in fact it has the advantage that .method (instead of ::method) is familiar.
|
We often write Anything saying Inherent methods would only make sense using some more elaborated syntax, like |
But both If we do want a more explicit synatax, I think |
| 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`. |
There was a problem hiding this comment.
Is it intentional or overlooked that Path (TypePath above) may resolve to a type?
|
|
||
| * 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. |
There was a problem hiding this comment.
I'd resolve this ambiguity by only allowing traits or the keyword Self in the expr.<XXX>::method(), not types. (Which seems to be what the RFC does)
To see if Self in obj.<Self>::method() is easily confused with "Self of obj" or "Self of current impl/trait block" would require testing the syntax on real-world samples. Could someone give a sample that mixes the two meanings?
There was a problem hiding this comment.
| 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; |
There was a problem hiding this comment.
Author probably meant use resources::*?
There was a problem hiding this comment.
Edit:
| use resources; | |
| use resources::sources::*; |
| * Resolves to `<Type as Trait>::method(obj, args)`. | ||
|
|
||
| * **Case: `obj.Self::method(...)`** | ||
| * Resolves to `<Type>::method(obj, args)`. |
There was a problem hiding this comment.
There's more than that, like method auto-(de)ref semantics.
It may be useful to note that the Type::method(args), Trait::method(args) and <Type as Trait>::method(args) syntax do not have auto-(de)ref semantics and I'd love to have this RFC support that (which is probably what the author meant). (Edit: See @teor2345's plauground link for a more complete demo)
There was a problem hiding this comment.
It may be useful to note that the
Type::method(args)syntax and the<Type as Trait>::method(args)syntax do not have auto-(de)ref semantics and I'd love to have this RFC support that (which is probably what the author meant).
It might be easier to see what's happening with Human::fly as well:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=bae17582995015650dd82fb248f1d3be
There was a problem hiding this comment.
What are you trying to say? Confirm that Human::fly syntax doesn't have auto (de)ref semantics as well?
There was a problem hiding this comment.
Yes, the example is much clearer with a call to Human::fly. If you're only calling traits like that, it could look like it's only about trait behaviour. But it's actually about any method call, with any type, using :: rather than .
There was a problem hiding this comment.
So you think auto-(de)ref semantics should be disabled in this RFC?
There was a problem hiding this comment.
By the way, you talked about Human::fly syntax, but actually I mentioned that in the case Type::method(args)
There was a problem hiding this comment.
So you think auto-(de)ref semantics should be disabled in this RFC?
I have no opinion on that yet, I was just talking about making the example playground clearer
There was a problem hiding this comment.
Oh, sorry. I misunderstood. Looking at my first comment, it will also be good to add that Trait::method(args) doesn't have auto-deref semantics. Will edit it.
There was a problem hiding this comment.
Overlooked R210 where author mentioned "ensure correct autoref/autoderef behavior"... But actually it's a strange way of saying it.
<Type as Path>::method(obj, args), which the new syntax is designed to desugar into, doesn't have method auto-(de)ref. Author might mean "figure out the correct Type&method to call using auto-(de)ref; and then it is equivalent to <Type as Path>::method(obj, args)". That would be convoluted to implement, especially that it feels like doing method resolution during syntax desugaring(?). I've put forward a suggestion for it.
|
The existing UFCS syntax, and many of the alternatives proposed, miss the crucial detail of the Having the trait apply to the type after the dereference is very important.
This is flawed in UFCS: This also means that syntaxes like A syntax like Color of the shed after the |
But how is this syntax going to give a method name? Or you're just giving a sketch of an idea, I guess? Considering the current |
|
Isn't deref free in UFCS now using |
|
@burdges If you mean "UFCS doesn't stop you from using deref", then it's true, but it's manual deref, and we were talking about method auto-(de)ref. Suppose one day you decide to switch from |
|
I think * should not be considered a manual deref per se, since it automates many things vs We're not discussing |
Rare, arguably, but here's a use case where it can be used to resolve a method name conflict between I'm not into the "It's rare so we don't need to optimize for it" way of thinking. And in the particular case of
Have you actually read the RFC? |
Can you point to a part of the RFC that contains the information that's been missed? Let's try to focus on explaining the RFC, and making it better, rather than getting distracted by what other people have (or haven't) done to understand it. |
studyingegret
left a comment
There was a problem hiding this comment.
Continuing previous discussion
| * Resolves to `<Type as Trait>::method(obj, args)`. | ||
|
|
||
| * **Case: `obj.Self::method(...)`** | ||
| * Resolves to `<Type>::method(obj, args)`. |
There was a problem hiding this comment.
Overlooked R210 where author mentioned "ensure correct autoref/autoderef behavior"... But actually it's a strange way of saying it.
<Type as Path>::method(obj, args), which the new syntax is designed to desugar into, doesn't have method auto-(de)ref. Author might mean "figure out the correct Type&method to call using auto-(de)ref; and then it is equivalent to <Type as Path>::method(obj, args)". That would be convoluted to implement, especially that it feels like doing method resolution during syntax desugaring(?). I've put forward a suggestion for it.
| ### 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)`. |
There was a problem hiding this comment.
| ### 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)`. | |
| The [`MethodCallExpression`](https://doc.rust-lang.org/reference/expressions/method-call-expr.html#railroad-MethodCallExpression) grammar is extended in two ways: | |
| 1. Explicit Trait Method Calls: `Expr '.' '<' TypePath[except 'Self'] '>' '::' Ident '(' Args ')'` | |
| * `TypePath` must resolve to a trait and must not be `Self`. | |
| * Semantics: `expr.<Trait>::method(...)`: Same as `expr.method(args)`, except that during method selection: | |
| * Only consider methods coming from the trait `Trait`. | |
| 2. Explicit Inherent Method Calls: `Expr '.' 'Self' '::' Ident '(' Args ')'` | |
| * Semantics: `expr.Self::method(...)`: Same as `expr.method(args)`, except that during method selection: | |
| * Only consider inherent methods. |
There was a problem hiding this comment.
I wonder if we could avoid most of the need for this RFC by instead extending RFC 3913 with the idea that
use Trait::item as m;will not only allow you to write m(x, y, z) then [for Trait::item(x, y, z)] but additionally allowing – if item was a method (i.e. with a self parameter) – to also then write x.m(y, z) now, calling the method, using method syntax, under its new name (the name introduced with the renaming use). This way, if foo.method() ends up being unfortunately ambiguous, then with a use path::to::Trait::{self, method as trait_method}; you can set up a disambiguated option of foo.trait_method().
Maybe we could also consider giving priority of all explicitly imported methods over those that are implicitly brought into scope (from a simple use path::to::Trait) and this can already help users to get some degree of added robustness against future breakage.1 The whole Motivation part 2 “Silent bugs and Fragility” IMHO is kinda super weird anyway, since adding optional additional syntax - as this RFC here proposes - does not actually do anything to solve any problem, right!?2
Thinking through this idea a little further, presumably one could even consider allowing something like use Arc::clone as clone_arc; and then people can write foo.clone_arc() instead of Arc::clone(&foo) if they want to make explicit that a (cheap) Arc reference is being copied, not the underlying/target type. [On that note, I expect that if this RFC here were to land, in practice, people may very likely end up annoyed why something like foo.<Arc>::clone() doesn’t work 😉.] If we’d like to get really creative, it might even be feasible to allow other kinds of type-defaults as renaming imports; (recognizing that Arc::clone is essentially a restriction of <T as Clone>::clone to the case where T is a type of the form Arc<_>); and allow e.g. use Iterator::{collect<Vec<_>> as collect_vec}; for people to then write foo.iter().map(…).collect_vec() [but without needing something like Itertools for this convenience] instead of a more clunky ….collect::<Vec<_>>() (which may be a nice change when it comes up multiple times).
Footnotes
-
You probably would not want priority of such explicitly imported items over inherent methods (since e.g. unsafe macros may want to rely on inherent methods being non-overrideable), though it may seem conceivable to at least give them equal priority, to at least avoid any silent behavior changes. ↩
-
IMO an ideal, and more fundamental, solution to the general fragility problem here may look like allowing the compiler so know what methods are newly added in what versions of a library, and then allow crates that depend on some library at version
0.1.1to be compiled in a way that acts as if all things that are newly added with library at version0.1.2didn’t actually exist yet. (This additionally would help tremendously with accidentally-too-low semver lower bounds in dependency lists [which may result in breakage for users further downstream if paired with MSRV constraints that may rule out0.1.2of the transitive dependency.]) ↩
There was a problem hiding this comment.
additionally allowing – if
itemwas a method (i.e. with aselfparameter) – to also then writex.m(y, z)now, calling the method, using method syntax, under its new name (the name introduced with the renaminguse). This way, iffoo.method()ends up being unfortunately ambiguous, then with ause path::to::Trait::{self, method as trait_method};you can set up a disambiguated option offoo.trait_method().
To me this has a feel like C++'s argument-dependent lookup, in that which function being called can be changed in some unexpected place. So please no.
There was a problem hiding this comment.
2. IMO an ideal, and more fundamental, solution to the general fragility problem here may look like allowing the compiler so know what methods are newly added in what versions of a library, and then allow crates that depend on some library at version
0.1.1to be compiled in a way that acts as if all things that are newly added with library at version0.1.2didn’t actually exist yet. (This additionally would help tremendously with accidentally-too-low semver lower bounds in dependency lists [which may result in breakage for users further downstream if paired with MSRV constraints that may rule out0.1.2of the transitive dependency.])
This would be extremely challenging to implement, because:
- the compiler doesn't handle crate versions, cargo does
- cargo only downloads the code of the one crate version it actually wants to use (and there's no guarantee that other versions of the code are available anywhere, let alone locally)
- comparing method availability between crate versions is a tricky problem, because you have to account for methods that are moved between files, modified, or deleted (and possibly other changes)
- this could be done with metadata, but then you're relying on both crate versions being able to be compiled with the current compiler arguments
It also seems like it would cause unexpected subtle behaviour changes, which can't be easily understood from reading the code locally. Ideally, reviews should only have to look at the caller to see what's being called. But this would require looking at multiple versions of crate code to determine what was actually being called.
There was a problem hiding this comment.
Arcis a type, not a trait... Is it that confusing indeed?
Yes, but that doesn’t wouldn’t make the desire for postfix notation any less relevant, does it?
Prefex, with current universal function call syntax, you can do either Clone::clone(…) or Arc::clone(…) (or <Arc<_> as Clone>::clone(…), so I don’t really think users would understand why a new postfix syntax only supports one option.
Looking back at comparing to the existing prefix notation, I’m furthermore noticing that .<Clone>::clone()-style syntax is particularly inconsistent with the existing prefix syntax since a trait on its own:
Here’s a quick outline of the supported syntaxes in current prefix UFC notation
let x = [1, 2, 3].iter();
use std::slice::Iter as SliceIter;
// you can traits or types direct (unbacketed)
let y1 = SliceIter::as_ref(&x);
let y2 = AsRef::as_ref(&x);
// you can optionally add the parameters
let y3 = SliceIter::<i32>::as_ref(&x);
let y4 = AsRef::<[i32]>::as_ref(&x);
// you can use the full `<Type as Trait>` syntax
let y5 = <SliceIter<i32> as AsRef<[i32]>>::as_ref(&x);
// you can leave parameters inferred within `<Type as Trait>`
let y6 = <SliceIter<_> as AsRef<[i32]>>::as_ref(&x);
let y7 = <SliceIter<i32> as AsRef<_>>::as_ref(&x);
let y8 = <SliceIter<_> as AsRef<_>>::as_ref(&x);
// but not fully omit them
// let y9 = <SliceIter as AsRef<_>>::as_ref(&x); // error: missing generics …
// let y10 = <SliceIter<_> as AsRef>::as_ref(&x); // error: missing generics …
// you can leave out the type with `_`
let y11 = <_ as AsRef<_>>::as_ref(&x);
// but not the trait
// let y12 = <SliceIter<_> as _>::as_ref(&x); // expected identifier, found reserved identifier `_`
// this latter is expressed instead by leaving off the `as …` entirely
let y13 = <SliceIter<_>>::as_ref(&x);
// even the type is still omittable here
let y14 = <_>::as_ref(&x);
// you CANNOT omit the `… as` prefix and only mention the trait
// within `<…>` brackets:
// let y15 = <AsRef<_>>::as_ref(&x); // error: expected a type, found a trait
// this instead must be written as in `y11` or as in `y2` above ;-)Note how < … >::method-style notation (with angle brackets, but without an as) only supports types and does not support traits, which turns out to be exactly the opposite of the proposed postfix alternative where only a trait goes between the brackets.
There was a problem hiding this comment.
The whole Motivation part 2 “Silent bugs and Fragility” IMHO is kinda super weird anyway, since adding optional additional syntax - as this RFC here proposes - does not actually do anything to solve any problem, right!?
Agree, very subtle problem. (Link added by me)
The [trait/inherent] method disambiguation problem can be added as background of the RFC (showing why method disambiguation is neccessary), but the RFC should be syntax-focused, other than its new semantic of inherent method restriction expr.Self::method(...).
A little off-topic, but since one new semantic is added, what about designing the RFC to allow for a family of future semantics, like allowing an impl block overriding a defaulted method to call the default version, maybe expr.<default>::method() or expr.<Trait default>::method()?
There was a problem hiding this comment.
so I don’t really think users would understand why a new postfix syntax only supports one option.
Are you talking about the option to write foo.<Arc as Clone>::clone() for Arc::clone(foo), Clone::clone(foo)? If that's what you mean, it looks simply complex to think about (do we auto(de)ref until the value type is Arc then apply usual UFCS semantics Clone::clone(drefed_foo)1, or do we skip auto(de)ref and desugar to Arc::clone(foo)?)
That kind of touches the question of relation between the RFC and traditional UFCS, kinda feels like defining the vision of the RFC: (the following points do not consider inherent method restrictions expr.Self::method(), as it is unique to the RFC)
-
Be a postfix "copy" of traditional UFCS, which means disabling auto(de)ref - no!! We'll have to add
&/*manually, which means spaghetti nesting left brackets:(&** (&mut (&a ).<Trait1>::method() ).<Trait2>::method() ).<Trait3>::method() (&** (&mut (&a ).<Trait1>::method() ).<Trait2>::method() ).<Trait3>::method() // Much better! a .<Trait1>::method() .<Trait2>::method() .<Trait3>::method()
-
Be a postfix "variant" of traditional UFCS that enables auto(de)ref -
The new syntax and the old UFCS will not be equivalent unless you can- enable auto(de)ref for old UFCS, and
- disable auto(de)ref for the new syntax
If we don't add these 2 features (write them into some RFC), it's mentally inconsistent (e.g. "why do I have to switch to postfix just to enable auto(de)ref?"); if we add them, there will be two separate code styles/conventions
- However it might also turn out that people turn to the new syntax and gradually phase out the old UFCS, like how
MaybeUninitreplacesuninitialized(?? don't know about it)
-
Just be a new syntax that doesn't care about relation with traditional UFCS - that seems to be what RFC author (and, personally, I) have been doing, but it's kinda disproved by @steffahn.
-
... anything else?
It makes me lost, what will the Rust devs want in order to accept a postfix syntax? My vision is a syntax that 1) is postfix 2) includes autoderef and it still feels worth pushing ahead, and it was wanted by some devs too.
Footnotes
-
I wanted to write "(de)refed_foo" as variable name but I find the brackets cumbersome to translate into variable conventions (
de_refed_foolooks not good enough), so I chose "dref". ↩
There was a problem hiding this comment.
Also come to think of it, even if people are familiar with Rc::clone() syntax, it doesn't really escape the chaining problem, though it maybe seems too rare to be a good tradeoff
Rc::new(
howdy()
.give_me_five()
).give_me_ten()🤔
|
|
||
| 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())` |
There was a problem hiding this comment.
With the angle brackets around a trait, this is not correctly representing the current syntax actually.
| 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. |
There was a problem hiding this comment.
This kinda reads like a bunch of overly big-sounding but fairly meaningless words to me 😕
I happen to know both Haskell and the topic of orphan rules and coherence rules fairly well and I have absolutely zero clue how they may related to this RFC that’s ultimately about surface syntax of method calls? I also have zero clue what kind of “ambiguities” and “code restructuring” that statement which is [among other things] apparently trying to say something about type-classes in Haskell is actually referring to.
I feel like the “Many other languages” line might either just be very badly worded and trying to express something completely different, or it’s made with way too little knowledge about the concrete “other languages” being discussed, or perhaps this is all just coming out of some sort of LLM hallucination?
There was a problem hiding this comment.
I feel like the “Many other languages” line might either just be very badly worded and trying to express something completely different
I guess this line is written by AI? (As @steffahn mentioned)
I happen to know both Haskell and the topic of orphan rules and coherence rules fairly well and I have absolutely zero clue how they may related to this RFC that’s ultimately about surface syntax of method calls?
There was a problem hiding this comment.
I can't get the images to load in the last post, but yes, that section was written by a Large Language Model:
#3913 (comment)
I thought it was a bit weird when I read it, but I didn't know most of the languages well enough to correct it.
Maybe if we don't have people to review LLM output, it shouldn't go in RFCs?




This RFC proposes two new forms of method call syntax for method name disambiguation that keep the receiver on the left and preserve chaining. In doing so, it takes a step towards making implicit selection between trait methods and inherent implementations optional.
An example of a chain of calls using proposed syntax:
obj .Self::chain() .<Trait1>::of() .<Trait2>::method() .<Trait1>::calls();Important
When responding to RFCs, try to use inline review comments (it is possible to leave an inline review comment for the entire file at the top) instead of direct comments for normal comments and keep normal comments for procedural matters like starting FCPs.
This keeps the discussion more organized.
Rendered