Trait return impl of trait with additional type parameter

I read several answers about returning impl of trait from Trait methods. Almost every people mentioned associated type. I would like to ask what if we cannot define associated type because at that time not all type parameters are available.

trait OperationState {
    fn start(self);
}

struct JustOperationState<T, R> {
    value: T,
    receiver: R,
}

impl<T, R> OperationState for JustOperationState<T, R>
where R: Receiver<Input = T>
{
    fn start(self) {
        self.receiver.set_value(self.value);
    }
}
pub trait Sender {
    type Output: Send + 'static;

    fn connect<R>(self, receiver: R) -> impl OperationState     //<-- here
    where
        R: Receiver<Input = Self::Output> + Send + 'static;
}

pub struct Just<T> {
    value: T,
}

impl<T> Sender for Just<T>
where
    T: Send + 'static,
{
    type Output = T;

    fn connect<R>(self, receiver: R) -> JustOperationState<T, R>   //<-- here
        where
        R: Receiver<Input = Self::Output> + Send + 'static,
    {
        JustOperationState::new(self, receiver)
    }
}

I don't want to use dyn because that may incur overhead.

Background: I'm trying to mimic the design of std::execution of c++23 (D2300R4: `std::execution`)

This kind of limitation, that an associated type cannot depend on additional generic arguments of methods, is eventually going to be addressed by generic associated types:

#![feature(generic_associated_types)]
pub trait Sender {
    type Output: Send + 'static;

    type ConnectReturnType<R>: OperationState
    where
        R: Receiver<Input = Self::Output> + Send + 'static;

    fn connect<R>(self, receiver: R) -> Self::ConnectReturnType<R>
    where
        R: Receiver<Input = Self::Output> + Send + 'static;
}

struct Just<T>(T);

impl<T> Sender for Just<T>
where
    T: Send + 'static,
{
    type Output = T;

    type ConnectReturnType<R>
    where
        R: Receiver<Input = Self::Output> + Send + 'static,
    = JustOperationState<T, R>;

    fn connect<R>(self, receiver: R) -> JustOperationState<T, R>
    where
        R: Receiver<Input = Self::Output> + Send + 'static,
    {
        JustOperationState::new(self.0, receiver)
    }
}

(playground)


For now, in stable Rust, possible workarounds include:

1: Adding a R parameter to Sender (but this approach might be a bit problematic/inflexible, depending on the use-case)

pub trait Sender<R>
where
    R: Receiver<Input = Self::Output> + Send + 'static,
{
    type Output: Send + 'static;

    type ConnectReturnType: OperationState;

    fn connect(self, receiver: R) -> Self::ConnectReturnType;
}

struct Just<T>(T);

impl<T, R> Sender<R> for Just<T>
where
    T: Send + 'static,
    R: Receiver<Input = T> + Send + 'static,
{
    type Output = T;

    type ConnectReturnType = JustOperationState<T, R>;

    fn connect(self, receiver: R) -> JustOperationState<T, R> {
        JustOperationState::new(self.0, receiver)
    }
}

(playground)

2: Using trait objects (I wrote this before you added the “I don't want to use dyn” remark)

pub trait Sender {
    type Output: Send + 'static;

    fn connect<R>(self, receiver: R) -> Box<dyn OperationState>
    where
        R: Receiver<Input = Self::Output> + Send + 'static;
}

struct Just<T>(T);

impl<T> Sender for Just<T>
where
    T: Send + 'static,
{
    type Output = T;

    fn connect<R>(self, receiver: R) -> Box<dyn OperationState>
    where
        R: Receiver<Input = Self::Output> + Send + 'static,
    {
        Box::new(JustOperationState::new(self.0, receiver))
    }
}

(playground)

Instead of making the return type a Box<dyn OperationState>, it can also be a reasonable approach to make the argument a Box<dyn Receiver>.

1 Like

Wow. Thanks for your detailed solutions.

By the way, since I'm new to Rust, I would like to ask why associated type is needed in such a case instead of letting Trait methods return impl of other trait.

For ordinary functions something like foo<T>() -> impl SomeTrait { … } means

  • foo has a fixed known-at-compile-time return type (so that in particular there is no overhead at run-time compared to e.g. using dyn SomeTrait – well regarding “fixed”: it can depend on generic type arguments like the argument T here (also it can only depend on the type arguments, nothing else well, besides lifetime arguments, but the rules are slightly complicated)
  • the return type of foo is however left unspecified to the API user, except for the information that it implements the trait SomeTrait

The benefit of this future over the approach to “just write the return type explicitly” can be

  1. the return type can be changed in the future without breaking API changes
    • this can otherwise only be achieved by creating a wrapper struct that abstracts away the concrete type
  2. the return type can be an unnameable type like e.g. the type of a closure or the type of an async { … } block
    • as such, impl Trait-return types are commonly used for -> impl Fn(Foo) -> Bar or -> impl Future<Output = Foo>; but also for impl Iterator<Item = Baz>, because iterators created by methods like .map() also contain closure types. For the case of fn foo(arguments...) -> impl Future<Output = T>, there's also the more convenient, but mostly equivalent alternative of writing async fn foo(arguments...) -> T
  3. the return type might be a very complex type, and writing impl SomeTrait makes the API much cleaner / easier to read
    • of course, in this case just writing the lengthy type is always an alternative

One downside of -> impl Trait is that this return type itself becomes unnamable, too; this is why one sometimes wants to avoid impl Trait return types in cases where it's possible, like for 1. or 3. above. This shortcoming is going to be addressed by an unstable language feature called type_alias_impl_trait.


Back to traits: The natural extension of the rule above, where the return type was compile-time-known, depending on the generic arguments, for methods in a trait would be:

In trait Foo<S> { fn foo<T>() -> impl SomeTrait; }, the return type can depend on Self, S, and T, i.e. on Self, as well as the generic arguments of the trait and the method.

And that describes the relation to associated types: The way to define a type in a trait that depends on Self and the generic arguments of the trait is precisely done by using an associated type. If you also need dependence on additional generic arguments, like the generic arguments to the method, then you need generic associated types. The additional details of the impl Trait-feature regarding unnameable types, and the fact that the type is left unspecified to the API user, are mere technicalities that could also, alternatively, be achieved by using type_alias_impl_trait in an associated type.

For more notes on current developments / ideas / designwork on support for -> impl Trait return types in traits, see e.g. this page

impl Trait in traits - wg-async-foundations

and the stuff it links to, particularly the "exploration doc" at the bottom. The connection to async is, as mentioned above, that async fn (pretty much) desugars to -> impl Future<Output = ...>, and there's a high demand for supporting async fn in trait methods. In case you're interested in async fn in trait, also take a look at

why async fn in traits are hard - Baby Steps

most of the stuff listed there also relates to the more general case beyond just async fn.

3 Likes