Dealing with unconstrained type parameters in impl blocks

Hey, I stumbled over this while basically building a more generic version of a futures::sink::Sink<_> wrapper which I already implemented before.

Let me first clarify what I wanted to achieve:

I basically wanted a Sink<_> (lets call it WrapperSink) that accepts some Type, let's say Vec<u8>. This WrapperSink wraps a generic that should be constrained to Sink and in particular to a Sink<T> where T: From<Vec<u8>>. I had a hard time actually putting this to code. This first thing I came up with was something like this

trait Sink<T> {
    fn start_send(&mut self, data: T);
}

struct WrapperSink<S> {
    inner_sink: S,
}

impl<S, T> Sink<Vec<u8>> for WrapperSink<S>
where
    S: Sink<T>,
    T: From<Vec<u8>>
{
    fn start_send(&mut self, data: Vec<u8>) {
        self.inner_sink.start_send(data.into());
    }
}

here I used a simpler version of Sink<_>. You might immediately see what the problem here is, hence the topic name :wink: This fails to compile with the error message

error[E0207]: the type parameter `T` is not constrained by the impl trait, self type, or predicates
  --> src/main.rs:11:9
   |
11 | impl<S, T> Sink<Vec<u8>> for WrapperSink<S>
   |         ^ unconstrained type parameter

Since I don't want to waste peoples time I read E0207 (btw. I love this feature :wink: ). And sure enough it provided a solution for my problem -> Use PhantomData.

So sure enough with PhantomData it works.

use std::marker::PhantomData;

trait Sink<T> {
    fn start_send(&mut self, data: T);
}

struct WrapperSink<S, T> {
    inner_sink: S,
    _phantom: PhantomData<T>
}

impl<S, T> Sink<Vec<u8>> for WrapperSink<S, T>
where
    S: Sink<T>,
    T: From<Vec<u8>>
{
    fn start_send(&mut self, data: Vec<u8>) {
        self.inner_sink.start_send(data.into());
    }
}

While I should be happy that this works, I'm not really liking this solution. Using the real futures::sink::Sink that wraps self into a Pin requires me to additionally constrain T: Unpin since PhantomData<T> is only Unpin when T: Unpin (which I honestly don't really understand, after all it should be just a marker?! But I guess that's just a fundamental consequence of the type system. In Addition I'm really not that familiar with Pin and Unpin, but that's on my agenda :wink: ).

Is there another way to deal with this? Maybe somehow constrain the Type Parameter of the inner Sink like it would be possible with associated types?

I might be missing the obvious here :wink: I just started with Rust again after a very long break. But I was finally able to convince my employer and my colleagues to consider Rust for our new projects, so I'm slowly getting back into it.

Any help or guidance would be really appreciated.

I'd answer on just one part:

Of course it's a marker. From the type system point of view, PhantomData<T> behaves exactly the same as T. So, if putting a real T in your struct would make it not Unpin, so is putting a PhantomData<T>. Likewise, putting PhantomData<&'a mut T> would make the struct invariant in 'a, and putting PhantomData<*const ()> will make it not Send.

In your case, you probably can just implement Unpin for WrapperSink directly - if this makes sense from the type's semantics, of course.

1 Like

The issue is that the inner type could implement both Sink<T1> and Sink<T2>, where both T1 and T2 satisfy the conversion bound. To fix this, you need to restrict things so that even in this scenario the compiler can figure out which impementation to use. As an alternative to PhantomData, you could also define a trait with an associated parameter to use instead of Sink directly for the inner type:

trait SinkFrom<Src>: Sink<Self::Via> {
    type Via: From<Src>;
}
1 Like

Thanks for the explanation, I think I get it now :wink: However, your example results in a compiler error

trait Sink<T> {
    fn start_send(&mut self, data: T);
}

trait SinkFrom<Src>: Sink<Self::Via> {
    type Via: From<Src>;
}
error[E0391]: cycle detected when computing the supertraits of `SinkFrom`
 --> src/lib.rs:7:1
  |
7 | trait SinkFrom<Src>: Sink<Self::Via> {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: ...which again requires computing the supertraits of `SinkFrom`, completing the cycle
note: cycle used when collecting item types in top-level module
 --> src/lib.rs:7:1
  |
7 | trait SinkFrom<Src>: Sink<Self::Via> {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Apologies; it looks like the bound needs to use fully-qualified syntax:

trait SinkFrom<Src>: Sink<<Self as SinkFrom<Src>>::Via> {
    type Via: From<Src>;
}
1 Like

I think this case should be fixed by https://github.com/rust-lang/rust/pull/74130 (when it gets merged).

1 Like

This is just a nit, but it would make it invariant in T. It would still be covariant in 'a.

2 Likes

Needing PhantomData does not necessarily mean you need PhantomData<T>.

The official documentation doesn't do a great job of explaining this, but the type argument for PhantomData should be chosen to have the same semantics as the type that holds it. This includes variance as well as drop behavior and auto traits such as Unpin. That means you should not use PhantomData<T> unless you're writing a type that semantically contains a T.

I'm not sure PhantomData is the right solution at all, but if it is, you need to use a type argument that conveys the actual relationship between WrapperSink<_, T> and T. It doesn't contain a T, but it can consume a T, at least, that seems to be the intended purpose. So try PhantomData<fn(T)>. Function pointers have no drop behavior, and they are Sync, Send, and Unpin, so you don't need to add a T: Unpin bound to make WrapperSink<_, T>: Unpin.

PhantomData<fn(T)> is contravariant in T, which is correct for a struct that does not store or produce a T. If you're writing any unsafe code, you might need PhantomData<fn(T) -> T> instead, which is invariant in T. Invariance is always the safest choice, so if you're unsure you might want to start with that and relax it to PhantomData<fn(T)> only when you encounter lifetime errors.

3 Likes

Ah I see, thank you very much! But now I'm stuck with either implementing it like

impl<T> SinkFrom<Vec<u8>> for T where T: Sink<Bytes> {
    type Via = Bytes;
}

which would block other Sinks like

impl<T> SinkFrom<Vec<u8>> for T where T: Sink<Bytes> {
    type Via = Bytes;
}

// Conflicting Implementation
impl<T> SinkFrom<Vec<u8>> for T where T: Sink<Vec<u8>> {
    type Via = Vec<u8>;
}

or directly on the type to have a single implementation like

struct Foo;

impl Sink<Bytes> for Foo {
    ...
}

impl Sink<Vec<u8>> for Foo {
    ...
}

impl SinkFrom<Vec<u8>> for Foo {
    type Via = Bytes;
}

which would be rather tedious :wink: Maybe I'll stick to the PhantomData solution, even though I'm left a little bit confused by @trentj comment, guess I'll use the PhantomData<fn(T)> version then?! :confused::slight_smile:

Anyway, thank you all for the insights, I learned a lot today! @2e71828 I'll mark your answer as the solution, even though I might not use it in the end, but you offered me a good alternative!

Right; whichever method you choose, you’ll need to manually specify the intermediate type for the conversion. The only substantive difference is in who makes the choice:

  • PhantomData requires it be chosen by the holder of the wrapper type
  • Hard-coding it in impl Sink makes it a property of the wrapper type itself, and
  • Defining a trait makes it a property of the wrapped type.

They all work, and the best choice will be determined by how this wrapper type is used in the rest of your program.

2 Likes

I should probably have linked to Looking for a deeper understanding of PhantomData. And yeah, it can be subtle and confusing, but I think PhantomData<fn(T)> is the correct kind of PhantomData to use in this situation, if that's the route you want to take.

2 Likes

@2e71828 I'll go with PhantomData and see for myself if it is ergonomic :wink:

@trentj I'll make sure to give it a read, thanks for the link !