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.

1 Like

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 ..

3 Likes

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

2 Likes