rust icon indicating copy to clipboard operation
rust copied to clipboard

Generic trait with generic supertrait bound by associated type can't be used in trait bound with associated type

Open robinhundt opened this issue 3 years ago • 2 comments

I'm trying to create a Channel abstraction over Sink and Stream and encountered the following issue:

Given the following (simplified without Sink and Stream) code: Playground

trait GenericTrait<T> {}

trait Channel<I>: GenericTrait<Self::T> {
    type T;
}

trait Sender {
    type Msg;

    fn send<C>()
    where
        C: Channel<Self::Msg>;
}

impl<T> Sender for T {
    type Msg = ();

    fn send<C>()
    where
        C: Channel<Self::Msg>,
    {
    }
}

// This works
fn foo<I, C>(ch: C) where C: Channel<I> {}

The current output is:

error[[E0277]](https://doc.rust-lang.org/stable/error-index.html#E0277): the trait bound `C: Channel<()>` is not satisfied
  --> src/lib.rs:18:5
   |
18 | /     fn send<C>()
19 | |     where
20 | |         C: Channel<Self::Msg>,
   | |______________________________^ the trait `Channel<()>` is not implemented for `C`
   |
help: consider further restricting this bound
   |
20 |         C: Channel<Self::Msg> + Channel<()>,
   |                               +++++++++++++

error[[E0277]](https://doc.rust-lang.org/stable/error-index.html#E0277): the trait bound `C: Channel<()>` is not satisfied
  --> src/lib.rs:20:12
   |
20 |         C: Channel<Self::Msg>,
   |            ^^^^^^^^^^^^^^^^^^ the trait `Channel<()>` is not implemented for `C`
   |
help: consider further restricting this bound
   |
20 |         C: Channel<Self::Msg> + Channel<()>,
   |                               +++++++++++++

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground` due to 2 previous errors

The issue is present in stable, beta and nightly.

When either removing the generic parameter on GenericParameter<I>, the Self::T bound on trait Channel<I>: GenericTrait<Self::T> or the Self::Msg bound on the send method

fn send<C>()
    where
        C: Channel<Self::Msg>;

the code compiles.

I think this is at least a diagnostics bug, as the error message is quite unhelpful :D This also seems like a bug/limitation of the trait system?

This might be related/the same as #58231 or #57905, but I wasn't exactly sure.

robinhundt avatar Aug 05 '22 18:08 robinhundt

@rustbot claim

compiler-errors avatar Aug 05 '22 21:08 compiler-errors

The diagnostic needs to be suppressed in this case, but also I don't know why this doesn't work but it's probably due to normalization and substitution somewhere happening in the wrong order, since we clearly know what the expected/normalized param-env bound looks like :thinking:

compiler-errors avatar Aug 05 '22 23:08 compiler-errors

Ugh this is a really strange param env normalization bug.

compiler-errors avatar Aug 09 '22 04:08 compiler-errors

@robinhundt were you able to find any workaround for this? I'm running into this exact same issue, and I can't find a way to work around it while still expressing the relationships between the traits that I want to express.

Anders429 avatar Sep 10 '22 16:09 Anders429

@Anders429 Well, kind of.

As I wrote in the initial OP, I wanted to unify Sink and Stream in a Channel trait. What I wanted was to use Channel::T to refer to the error in a Stream of Result<Item, Error>. I noticed that the futures crate also offers the TryStream trait (link). I came up with the following:

trait Channel<Item>: Sink<Item> + TryStream<Ok = Item> {}

impl<Item, S> Channel<Item> for S where S: Sink<Item> + TryStream<Ok = Item> {}

which allows me to refer to the stream error as <Ch as TryStream>::Error. Not ideal, but it works. Unfortunately, this only works when the type we want to refer to appears in an associated type of the super trait (above, this is Stream::Item).

It appears I simplified code in the OP a bit too far. My solution with the TryStream does not work for a GenericTrait<T> as

trait GenericTrait<T> {}

trait ParamToAssoc {
    type Assoc;
}

impl<T, G> ParamToAssoc for G where G: GenericTrait<T> {
    type Assoc = T;
}

fails with an unconstrained type parameter error in the ParamToAssoc impl.

Maybe your use case is similar to mine and this helps you :blush:

robinhundt avatar Sep 12 '22 15:09 robinhundt

Well, it seems my solution of just referring to the associated type of the super trait does not work completely... :cry:

trait Sink<Item> {
    type Error;

    fn foo(&mut self) -> Result<Item, Self::Error> {
        todo!()
    }
}

trait Channel<Item>: Sink<Item> {}

// This works
fn use_channel<C>(ch: &mut C) -> Result<(), C::Error>
where
    C: Channel<()>,
    <C as Sink<()>>::Error: Send,
{
    // ch.foo()
    todo!()
}

trait UsingChannel {
    type Item;

    fn use_channel<C>(ch: &mut C) -> Result<Self::Item, C::Error>
    where
        // Commenting out the following line in the declaration and the impl
        // makes the code compile
        <C as Sink<Self::Item>>::Error: Send,
        C: Channel<Self::Item>;
}

struct Foo;

// --------- The Problem is in this impl ---------

impl UsingChannel for Foo {
    type Item = ();

    fn use_channel<C>(ch: &mut C) -> Result<Self::Item, C::Error>
    where
        C: Channel<Self::Item>,
        // commenting out this line makes the code compile
        <C as Sink<Self::Item>>::Error: Send,
    {
        // ch.foo()
        todo!()
    }
}

[Playground]

the code compiles without the <C as Sink<Self::Item>>::Error: Send bound on the trait, but adding it, results in the dreaded the trait bound "C: Channel<()>" is not satisfied message as well as the trait bound "C: Sink<()>" is not satisfied.

This makes it currently impossible for me to write an abstraction over Sink + Stream, as I need some way of constraining the corresponding error types.

robinhundt avatar Sep 30 '22 16:09 robinhundt

I've tried to further reduce the issue. Unfortunately, not even the following simplified code compiles:

trait Sink<Item> {
    type Error;
}


trait UsingSink {
    type Item;

    fn use_sink<S>(ch: &mut S) -> Result<Self::Item, S::Error>
    where
        S: Sink<Self::Item>,
        // Commenting out the following line in the declaration and the impl
        // makes the code compile
        S::Error: Send;
        
}

struct Foo;

// --------- The Problem is in this impl ---------

impl UsingSink for Foo {
    type Item = ();

    fn use_sink<S>(ch: &mut S) -> Result<Self::Item, S::Error>
    where
        S: Sink<Self::Item>,
        // commenting out this line makes the code compile
        S::Error: Send,
    {
        todo!()
    }
}

[Playground]

This reduces the usability of the Sink trait (or other traits with a generic parameter and associated type), as its associated type can't be bounded in another trait when the Item generic parameter is supplied by an associated type :cry:

For this simple case, a workaround exists by introducing a new generic parameter for the use_sink function, which must be equal to the Error associated type and can then be bounded. However, this is ugly and does not really work when the associated type is ambiguous, which is the case when the supertraits are Sink + TryStream.

robinhundt avatar Oct 01 '22 17:10 robinhundt