Proposal: Syntactic Sugar for Disambiguating Conflicting Trait Methods

I'm not an experienced Rust developer, I'm just learning the language, but here we're discussing syntactic sugar, so I don't think specific expertise is required.

Problem

#[derive(Debug, Clone)]
struct AnB {
    a: u32,
    b: u32
}

trait ArithmeticallyAddable {
    fn add(&self) -> u32;
}

trait Concatenatable {
    fn add(&self) -> u32;
}

impl ArithmeticallyAddable for AnB {
    fn add(&self) -> u32 {
        self.a + self.b
    }
}

impl Concatenatable for AnB {
    fn add(&self) -> u32 {
        match (self.a.to_string() + &self.b.to_string() as &str).parse::<u32>() {
            Ok(v)   => v,
            Err(..) => 67,
        }
    }
}

fn main() {
    let a = 2;
    let b = 2;
    let anb = AnB {a:a, b:b};
    println!("{a} + {b} = {}  if  ArithmeticallyAddable::add  was called", ArithmeticallyAddable::add(&anb));
    println!("{a} + {b} = { } if  Concatenatable::add         was called", Concatenatable::add(&anb));
}

This is how a name conflict in Rust is usually handled when it occurs. Quite ordinary code, but the used syntax strongly differs from the standard anb.add(). First, this is no longer a method call, second, here you need to specify that anb is passed explicitly as an immutable reference (&self), whereas in anb.add() the compiler specifies this automatically. (&v as &dyn Trait).method() solves the first but not the second, plus it's cumbersome, and v.Trait::method() simply does not exist in Rust, which is reasonable (UPDATE: actually it's not, v.Something::something_else seems never to be used in Rust).

For such an idiomatic language as Rust, where everything is logical (or supposed to be), it's odd that the penalty for poor naming is not just an additional specification of the specific trait whose method is used, but a complete change in the method call syntax, as well as a requirement to do manual work the compiler would handle.

Proposed Solution

  1. Currently syntax (v as Trait).method() results in an error expected a type, found a trait. If replaced with ambiguous method name resolution, no existing code would break.

  2. For multiple use, use Trait::method for Type; should make method from Trait the default option for all values of type Type across the scope.

Show some activity under the topic if you have any ideas or you simply agree with the fact that this should be added to the language

UPDATE

At first glance, v.<Trait::method>() looked as ugly to me as v.Trait::method(), but I looked at it again and realized that it's clean and straightforward. It changes only the source of the ambiguity -- the method name - and clarifies that there's nothing called Trait, only <Trait::method> as a single symbol, so...

Proposed Solution No. 2

  1. v.<Trait::method>() is used for a single use

  2. For multiple uses, use Trait::method for Type; makes method from Trait the default option for all values of type Type across the scope. Specifying exact variables is also possible, e.g., use Trait::method for v;.

  3. The Trait symbol comes from the scope.

1 Like

I think adopting the C++ foo.Bar::baz() would actually not be a bad option.

5 Likes

The worry I'd have with (foo as Itertools).intersperse(3) is that it looks like you should be able to do let x = foo as Itertools; x.intersperse(3), so if that doesn't work I'm not a fan.

(Having as impl Itertools or something maybe could perhaps reasonably be made to work, but wouldn't solve the disambiguation problem because there result -- whether that way or as impl Iterator -- could still be called with methods from both traits. It would, I suppose, at least block calling inherent methods, if that was the goal.)

So I also prefer something like foo.Bar::bar(). I've also spelled it as foo.<Bar::bar>() before in sketches, aping the full desugaring of <Foo as Bar>::bar(&foo).

There's a big question here either way whether what's there is a full path, just an ident that's already used, or something that works even with use MyTrait as _; that's not actually putting anything in the namespace despite it allowing resolution.

3 Likes

As for let x = foo as Trait, it should convert foo: &Type into x: &dyn Trait (preserving the same mutability) or throw an error if foo is an owned value.

By the way, this approach makes it crystal clear where the Trait symbol comes from: you're explicitly bringing it into scope yourself and using it by the name you’ve set in the line use bimbumlib::bambammodule::SuperLongAndCompletelyUselessNameOfTheTrait as Whatever.

Do you know that dyn Trait used to be spelled Trait? Your idea here goes in the opposite direction, and implicitly turns on dynamic dispatch.

1 Like

You are right. I think dyn is not necessary for (foo as Trait).method() (like in <Foo as Trait>), but dyn must be added, and foo must be a reference to be stored or passed. Traits cannot be owned values, so it would be obvious that (&mut foo as dyn Trait) is a &mut dyn Trait.

I think having foo as Trait turn foo into an existential is better.

But that's not very interesting. What I think is pretty interesting, is that if we had higher kinded polymorphism, this literally could've been a library function like

fn as_impl<trait T>(val: impl T) -> impl T {
    val
}
3 Likes

This would limit you to dyn-compatible traits and their Self: ?Sized methods.

And why would you want as for this instead of let x: &dyn Trait = foo?

I was distracted. My current proposal is to leave the as keyword as it is and use foo.<bar::baz>() for a single use (because it only covers the method name part, which was the initial problem) and use bar::baz for foo1 or use bar::baz for Foo for multiple uses. The trait symbol should come from the scope and not from Foo or anywhere else.

A use Trait for Type syntax has the same problem as CSS's !important — raising the priority level by one only helps that once. Once you have two high priority trait impls, how you you specify which is higher priority for their overlap?

The var.<Trait::method>() has the problem that <path> indicates an item in the type namespace, whereas Trait::method is in the expression namespace. A more "correct" spelling of this turbofish variant might then be var.<Trait>::method(). This could be an improvement over Trait::method(&var)… but if so, not by much. And the edit difference from var<Trait>::method() is scary for diagnostics.[1]

As awkward as the var.(Trait::method)() syntax feels, I still feel like it's the least bad option available.[2] The (path) form clearly takes an expression item. And as a bonus, it can take any expression, making it possible to invoke any function expression in LTR . pipelining with method autoref semantics, when that's desirable.


  1. Did you mean:

    • <var<Trait>>::method()
    • var::<Trait>::method()
    • var.<Trait>::method()
    ↩︎
  2. And yes, I'm immediately violating my own concern about edit difference from var(Trait::method)(), which even is more likely to be potentially valid, since types don't use snake_case names, but functions do. My counter to that is that the turbofish is a very common stumbling block for new developers, since other languages decide to just make syntax work without the turbofish. We want to empower diagnostics to realize that error and recover parsing reasonably. var.(func) doesn't have that same significant parsing drift between the "typo" options, meaning it's "just" recovery by looking in the other namespace. ↩︎

2 Likes

I agree. But the syntax should probably be value as impl Trait to make it more distinct from dyn Trait.

Ah, that is starting to smell familiar.

Here's some recent discussion on enabling use of arbitrary functions in method chains:

Zulip topic

2 Likes

Yet again we've converged on the same solution:

https://internals.rust-lang.org/t/weird-syntax-idea-s-for-umcs/19200

Btw some time ago I made for myself a simple prototype of this idea:

https://crates.io/crates/pipei

Pondering: it'd also be nice if whatever we did here allowed "calling base" in an implementation, too. Today if you want to have an impl sometimes handle it but otherwise fallback to the provided one, it's annoying.

Well, there's a separate RFC PR for that RFC: calling default trait methods from overriding impls by adamcrume · Pull Request #3329 · rust-lang/rfcs · GitHub