2

This code:

trait SayHello {
    fn say_hello() { println!("hello") }
}

trait Foo {
    type Hello: SayHello;
}

trait Bar: Foo {
    type Hello: SayHello;
}

struct Generic<T>(T);

impl<T> Generic<T> where T: Bar<Hello = <T as Foo>::Hello> {
    fn say_hello() {
        T::Hello::say_hello()
    }
}

returns this error:

error[E0221]: ambiguous associated type `Hello` in bounds of `T`
  --> src/lib.rs:17:9
   |
6  |     type Hello: SayHello;
   |     --------------------- ambiguous `Hello` from `Foo`
...
10 |     type Hello: SayHello;
   |     --------------------- ambiguous `Hello` from `Bar`
...
17 |         T::Hello::say_hello()
   |         ^^^^^^^^^^^^^^^^^^^ ambiguous associated type `Hello`
   |
help: use fully qualified syntax to disambiguate
   |
17 |         <T as Foo>::Hello()
   |         ^^^^^^^^^^^^^^^^^
help: use fully qualified syntax to disambiguate
   |
17 |         <T as Bar>::Hello()
   |         ^^^^^^^^^^^^^^^^^

But there shouldn't be any ambiguity. It's clearly stated that the Hello associated type is the same in Foo and bar.

  • Why is there an issue?
  • Is there a way to make this logic work?
Boiethios
  • 38,438
  • 19
  • 134
  • 183
  • 1
    Bounds can be arbitrarily complex, so the compiler doesn't even try to reason on "Oh but wait, those types happen to be equal" level. It would be easy in that particular case (or would it?), but there's always going to be someone trying one level further than what's implemented. – mcarton Mar 15 '21 at 17:02
  • @mcarton I think that some languages like Haskell "understand" this kind of bounds and allow such reasoning, but I may be wrong. Anyway, it's "just" a logical system where it's possible to answer the question if 2 types are similar. – Boiethios Mar 15 '21 at 17:07
  • 2
    It's not that the compiler can't tell that two types are the same, because you're writing generic code -- there *are* no types, only type *parameters*. Admittedly, a sufficiently smart compiler could recognize that the constraints on the parameters guarantee they are in fact the same type. However, that's still a harder problem than knowing that two *resolved* types are the same. "Queen Elizabeth II is Queen Elizabeth II" is a tautology; "the Monarch of the United Kingdom is the Monarch of Canada" requires higher-order logic even though all three phrases refer to the same person. – trent Mar 15 '21 at 18:12
  • @trentcl So, the answer is just that the type system isn't powerful enough. Prolog or any other logical language can say if something is true/false/indetermined from a set of rules. – Boiethios Mar 15 '21 at 18:19
  • 2
    No, actually, I think the answer is that `T::Hello` is simply ambiguous at name resolution time. Name resolution doesn't care about what the types *actually are*, only about the names you use to get to them. This is also why you can call `.borrow()` on a function argument of type `T: Borrow` and get `&str` instead of `&T`, even though `T` definitely also implements `Borrow` -- name resolution only cares about what's declared, not what can be inferred. – trent Mar 15 '21 at 18:56
  • 2
    In other words, even if the type system *were* powerful enough to prove that `::Hello` and `::Hello` are the same (I don't think it is), it wouldn't matter, because `T::Hello` still has to refer unambiguously to one or the other. Bear in mind `T::Hello` is really just a convenient shorthand for `::Hello` when `Trait` is unambiguous. Here it is ambiguous. – trent Mar 15 '21 at 18:59
  • @trentcl Alright, it's clearer now. You should update the answer or create your own. – Boiethios Mar 15 '21 at 21:05

1 Answers1

4

If T implements Bar, it also has to implement Foo, as per your bound on the trait. Therefore, T::Hello in say_hello() can both refer to <T as Foo>::Hello or <T as Bar>::Hello, which fixes your example.

It'd be possible to introduce another parameter H: SayHello and set both Foo::Hello and Bar::Hello to H. Dispatch to say_hello() then works via H::say_hello(), rather than T::Hello::say_hello().

struct Generic<T>(T);

impl<T, H> Generic<T> where T: Bar<Hello=H> + Foo<Hello=H>, H: SayHello {
    fn say_hello() {
        H::say_hello()
    }
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=920368d7c2d4e5eea98465de8b193b9e

kmdreko
  • 42,554
  • 6
  • 57
  • 106
sebpuetz
  • 2,430
  • 1
  • 7
  • 15
  • My example isn't complete, but there can also be a `Generic` implementation where that bound isn't needed, *aka* where `Foo::Hello` isn't the same as `Bar::Hello`. Your solution prevents such a thing to exist. – Boiethios Mar 15 '21 at 17:03
  • In that case the second paragraph of my answer should work: specify `T::Hello` as `::Hello` – sebpuetz Mar 15 '21 at 17:06
  • Well, yes, that's the modification proposed by the compiler, but that doesnt answer my first question – Boiethios Mar 15 '21 at 17:11
  • Yep, sorry, I didn't properly read the example. There's another way to express the constraints via `impl Generic where T: Bar + Foo, H: SayHello` and `H::say_hello()`, which makes it clear to the compiler that both `Foo::Hello` and `Bar::Hello` are `H`. – sebpuetz Mar 15 '21 at 17:43
  • Hey, that's a clever trick, thanks! I still wonder why the compiler doesn't understand my code – Boiethios Mar 15 '21 at 18:01
  • I think the original answer is still essentially correct; see my comments on the question – trent Mar 15 '21 at 19:00