Restricting Traits

I've recently been playing around with tower, and in particular the Service, which I've copied over verbatim below:

pub trait Service<Request> {
    type Response;
    type Error;
    type Future: Future;
    fn poll_ready(
        &mut self, 
        cx: &mut Context<'_>
    ) -> Poll<Result<(), Self::Error>>;
    fn call(&mut self, req: Request) -> Self::Future;
}

My problem is that I want to created a restricted version of this trait, something like the following:

pub struct MyRequest;
pub struct MyResponse;
pub struct MyErr;

pub trait MyTrait: Service<MyRequest> {
    type Response: MyResponse;
    type Error: MyErr;
    type Future: Future;
    fn poll_ready(
        &mut self, 
        cx: &mut Context<'_>
    ) -> Poll<Result<(), Self::Error>>;
    fn call(&mut self, req: Request) -> Self::Future;
}

The idea is that implementations of MyTrait are restricted to a subset of what Service can be implemented as, leading to implementations similar to the following:

pub struct Foo;

impl MyTrait for Foo {
    type Future: Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
    
    fn poll_ready(
        &mut self, 
        cx: &mut Context<'_>
    ) -> Poll<Result<(), Self::Error>> {
        todo!();
    }
    fn call(&mut self, req: Request) -> Self::Future{
        todo!();
    }
}

which means that instead of writing a function like the following:

pub fn name<T>(_arg: T) -> ()
where
    T: Service<
        MyRequest,
        Response = MyResponse,
        Error = MyError,
        Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>,
    >
{
    unimplemented!()
}

You can instead write something like:

pub fn name<T>(_arg: T) -> ()
where
    T: MyTrait<Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>>
{
    unimplemented!()
}

The issue is that this doesn't currently work. For one thing, it conflicts with another good idea, RFC 2532, "Associated type defaults". For another, Rust traits are specifically designed so that C++ subclass-like behavior is forbidden. I suspect that changing this would be a major breaking change as it would imply that marker traits would be required to be non-empty, so I'm not suggesting it. The problem is that I can't think of something that would work well here. Can anyone else? Or is there a reason why syntactically different versions of this would be a bad idea?

Why don't you write it the way it currently is? I see no reason for adding new, different syntax for something that already has an exact equivalent. (And please don't tell me it's "simpler". It isn't.)

As far as I understand, you’re after two things here.

First, shortening bounds by writing MyTrait instead of the whole Service bound

This can already be achieved by defining a trait such as

pub trait MyTrait: Service<MyRequest, Response = MyResponse, Error = MyErr> {}

impl<T: ?Sized> MyTrait for T where T: Service<MyRequest, Response = MyResponse, Error = MyErr> {}

Second, a more concise way of implementing of the relevant Service<MyResponse> trait. This is where Rust’s trait system still has room for improvement IMO, as something like

impl<T: ?Sized> Service<MyRequest> for T
where
    T: ImplementMyTrait,
{
    type Response = MyResponse;
    type Error = MyErr;
    type Future = <Self as ImplementMyTrait>::Future;
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        ImplementMyTrait::poll_ready(self, cx)
    }
    fn call(&mut self, req: MyRequest) -> Self::Future {
        ImplementMyTrait::call(self, req)
    }
}

Is currently not accepted. (Even though IMO, it should. Provided it happens in the same crate that defines the ImplementMyTrait trait, and with the semantics that it restricts ImplementMyTrait implementations to only be allowed whenever the implied Service<MyRequest> implementation is allowed, too, w.r.t orphan rules.)

The only workarounds are:

  • Require a wrapper type, so some
    impl<T: ?Sized> Service<MyRequest> for Wrapped<T>
    where
        T: ImplementMyTrait,
    
  • Require usage of a macro. Here’s a lengthy example of how that could look like.

Making these kinds of things more ergonomics – including a way to give both the trait that’s used for bounds, and the one that’s used for shorter impls the same name – should be possible with some rough feature ideas of mine that I’ve mentioned in this post. I don’t have any time this month to work developing those further.

2 Likes

Indeed, that would be a much clearer way of writing that implementation. It looks like something that should "just work" (although I realize it's never quite as easy as that).

1 Like

Yup, you're right, I slipped a gear when thinking about this one. Thank you for reminding me that this is possible.

That's an interesting way of doing it, kind of the inverse of what I was thinking about, but it still gets the job done. I like it!

That said, what you say about the orphan rules is probably going to be real killer here. The issue is that I want to create a crate that defines this child trait publicly, so that users of my crate can use the simplified trait instead of the complex trait. However, that would violate the rule you propose. Unless there is some way of ensuring that the orphan rules aren't violated, this just won't work.

As far as I understand the rules I proposed, it shouldn't be problematic. In case this was the point of the confusion:

I'm requiring that the generic implementation of

impl<T: ?Sized> Service<MyRequest> for T
where
    T: ImplementMyTrait,

has to happen in the same crate that defines ImplementMyTrait, whereas implementations like impl ImplementMyTrait for Foo are allowed downstream. I envision that such a setting should, to downstream users, look/behave pretty much [1] the same way as something like the From/Into hierarchy behaves: You can only implement From<Foo> for Bar if there isn't any existing conflicting Foo: Into<Bar> implementation already.


  1. with some minor differences because From does AFAIK allow some implementations where orphan rules wouldn't quite allow you to write the implied Into impl directly ↩︎

2 Likes

Yup, that was the part I was confused about! Thank you for clarifying this.

Is it time to write an RFC now?

Well… as mentioned, I don’t have time this month. Given the high frequency of me coming across situations where the thing could be useful recently, so I might give it a go and work towards an RFC starting in April. Or at least better work out some of the details of the new rules, since it is indeed AFAICT really quite non-trivial feature in detail.

No problem, I fully understand about time! If you wish to, put it in your queue somewhere and when you have time, work on it. If you don't, please let me know and if my employer doesn't mind my working on it, I can take a stab at it.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.