Why can't you disambiguate via cast on a variable

Why can't you do (bar as Foo<4>).foo() for a trait Foo<const F:u8> much the same way you can do <Bar as Foo<4>>::foo() if bar were a type instead?
Foo::<4>::foo(bar) is not nearly as clear as (bar as Foo<4>).foo() in terms of communicating that foo is a trait instance method and not a function of some class Foo. Also I don't believe this causes any new ambiguity that wasn't something that would've errored anyway before, but I could be wrong.

Example of what I think it could look like and more on why I have issues with the current system:

1 Like

The former is a cast, the latter is called a fully qualified path. Even though they look similar, they are different language constructs for different purposes. Casts change the type of an expression, fully qualified paths are there to disambiguate method calls.

5 Likes

From the playground:

    println!("{}", (element as &mut Foo<18>).foo(16));

That's a bit different than (bar as [the trait] Foo<4>). There is no &mut Foo<18> trait, so above, you're talking about ($Expression as $Type) -- which is a cast, like @jofas mentioned.

Moreover, you may have noticed the suggestion

error[E0782]: expected a type, found a trait
  --> src/main.rs:33:37
   |
33 |     println!("{}", (element as &mut Foo<18>).foo(16));
   |                                     ^^^^^^^
   |
help: you can add the `dyn` keyword if you want a trait object
   |
33 |     println!("{}", (element as &mut dyn Foo<18>).foo(16));
   |                                     +++

Which does indeed compile, performing a cast to the &mut dyn Foo<18> type and[1] dynamically dispatching to the method. On edition 2015 and edition 2018, the dyn indicator is optional for backwards compatibility reasons,[2] and the original code compiles.

All in all, I think any variation on ($Expression as $...) is too ambiguous to fly, especially since some of them are valid code in older editions, which would be quite confusing. But maybe other possibilities would be entertained.

// Spit-balling
element.foo::<in Foo<18>>(101);

You could search IRLO to see if it's come up before.


As for alternatives that work today...

    // you also do this which is clearly but a lot bulkier but is clearer
    <Struct as Foo<18>>::foo(element, 101);

This usually also works.

    <_ as Foo<18>>::foo(element, 101);

For this particular example, you could also

pub trait Quz {
    fn quz<const X: usize>(&mut self, bar: u64) where Self: Foo<X> {
        self.foo(bar);
    }
}

impl<T: ?Sized> Quz for T {}

// ...

    element.quz::<18>(101);

  1. modulo optimization ↩︎

  2. the dyn indicator didn't used to exist and you just had to know if you were looking at a type context or a trait context, plus some corner cases ↩︎

3 Likes

The compiler literally tells you the reason:

error[E0782]: expected a type, found a trait
  --> src/main.rs:32:37
   |
32 |     println!("{}", (element as &mut Foo<18>).foo(16));
   |                                     ^^^^^^^
   |
help: you can add the `dyn` keyword if you want a trait object

And in fact, if you follow its suggestion and add the dyn keyword, then your example works as intended.

1 Like

You are 99.99% correct, but the answer is in 0.01% that you omitted. The 100% correct phrase would be:

FOR SOMEONE WHO DOESN'T KNOW RUST Foo::<4>::foo(bar) is not nearly as clear as (bar as Foo<4>).foo() in terms of communicating that foo is a trait instance method and not a function of some class Foo.

And that bolded part is the anwer. The problem here lies with the fact that bar as Foo<4> already have a meaning and it's absolutely different one from Bar as Foo<4>: it assumes that Foo is a type (most likely enum or struct) and you want to convert value of one type to another.

So now you want to propose third thing that would be expressed as “anythins as something”… thanks, but no, thanks.

We already have too many different things expressed as “anything as something”… adding another one wouldn't make code easier to read, rather an opposite.

At some point you have to accept that most people who read Rust program know Rust… making language less readable for them wouldn't be a good thing.

2 Likes

The current syntaxes unfortunately change invocation from a method call to a function call syntax, which has a side effect of lacking auto-deref magic.

However (value as T) has a similar problem.

When you call a method via ., you don't care if it's &mut V or &V or V or even another type with Deref<Target=V>. This is an important ergonomic feature in Rust.

But (value as Type) exists, and it does strictly care about the difference. Making it ignore references and autoderef would be a breaking change that introduces ambiguity. And requiring it to support (value as &mut Trait) is a chore, and a type/trait mix that Rust doesn't use anywhere else.

Rust could technically make (value as Trait) in new editions have more of the magic and behave more like match ergonomics, but having such wildly different behaviors for the same (value as T) syntax depend on what T is could be confusing and a footgun.

So Rust needs something after the . operator, to clarify what type/trait is wanted after dereferencing and coercions that happen in ..

4 Likes

And the most logical option would be value.Trait::method() like in C++.

2 Likes

Hmm... My personal flavor would be something like (x for Trait).method().

Any comments?

As for your flavor, I found this on internals forum: Proposal: Syntactic Sugar for Disambiguating Conflicting Trait Methods - language design - Rust Internals

That's not after the . operator. Rust could make a special case of (_ for _). being an operator that evaluates . first and then for Trait later, but it would be backwards and inconsistent with semantics of values and (…) elsewhere in the language.

The whole point is that x.method() doesn't call the method on x specifically, but on a dereference of x made by the . operator, which can be a different type. The type of x may not support a Trait, but a Deref of x may support a Trait. So (x for Trait) asks potentially a wrong type to support the Trait.

1 Like

I think I wasn't clear enough. What I was actually thinking is for (x for Trait) to only restrict method lookup to a trait, method deref semantics are unchanged. And it should only be usable in the form (x for Trait).method(), otherwise it may not make sense. My bad for not stating the whole idea.

However from your reply it seems that the syntax is a bit counterintiuitive (but not as it seems to OP).

Agree that it is backwards, but I kind of can't get how it is inconsistent with semantics of other (…).

One could probably consider (bar as &mut impl Foo<4>).foo() be made to work here… though really for your concrete use case & example, it seems like you care most about specifying the 4, not the Foo, anyway.

I’ve previously already thought, maybe we should allow some kind of “repeat declaration” of trait-level generic arguments on a method. (A little bit like you can already write Option::<T>::Some or Option::Some::<T>, just for methods, and of course with need for explicit opt-in.)

Something like, you can re-write

pub const trait Into<T>: Sized {
    fn into(self) -> T;
}

to

pub const trait Into<T>: Sized {
    fn into< =T >(self) -> T;
}

and now be allowed&able to simply write

let x = "foo".into::<String>();

if you wanted to specify the return type more comfortably here. [I am of course aware that for this particular case, x: String or using String::from are good alternatives, too, but it’s just an example, and not all use-cases for all traits/cases offer such nice alternatives]

Then similarly, for your example turning

pub trait Foo<const X: usize> {
    fn foo(&mut self, bar: u64) -> u64;
}

into

pub trait Foo<const X: usize> {
    fn foo<const =X>(&mut self, bar: u64) -> u64;
}

(or whatever the syntax would be)

introduces a second place where you’re allowed to specify X,
and thus you can now simply write bar.foo::<4>()

1 Like