-
Notifications
You must be signed in to change notification settings - Fork 282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Consider switching Service::Request
to a generic
#99
Comments
Service
be generic over Request
Service::Request
to a generic
@withoutboats Given your work w/ earlier work w/ |
FWIW, I'm taking a crack at making this change and updating the rest of the tower stack to work with it, in order to prove out the surface area of the change. |
At least hypothetically this makes sense: |
For anyone interested: this branch rewrites all of
Edit: as @withoutboats suggested, I've gone back and removed almost all the uses of |
@hawkw a lot of these phantomdata seem to be because you're applying the |
@withoutboats In most of these cases, the type bounds were already present in the original code; I tried not to remove any type bounds that were already present. That said, going back, you're right that there were a number of middleware types that didn't actually need to be parameterized over the request type of the wrapped service. I thought these were only added when the compiler actually told me they were necessary, but that doesn't seem to be the case --- I'm in the process of cleaning the branch up a bit. Thanks! |
@hawkw I know you didn't add them, but I believe if you removed them where they aren't strictly necessary you would probably be able to eliminate just about every PhantomData you had to add. :) |
@withoutboats Yup, I removed most of the unnecessary bounds (and thus, the PhantomDatas) as you suggested in ab8a528; it looks much nicer now. Thanks. Originally, I didn't want to remove the bounds just to keep the diff smaller, but I think it ended up being worth it. |
While it's been common for Rust to not add bounds for constructors and things, and only for the trait impls when it needs them, I've actually found that there's benefit in them being in the constructors for this reason: it helps tame the errors produced by the compiler. It's easier to notice the error saying |
For those watching from home: this branch rewrites Writing these branches was mostly pretty painless, aside from one issue that threw me off for a bit --- generic type parameters in a blanket impl of a trait for |
I actually agree w/ you. What I have done (when I took the time) is put constructors in dedicated impl<T: Foo> {
pub fn new(foo: T) -> Self { ... }
}
impl<T> {
pub fn get_ref(&self) -> &T { ... }
} It works pretty well. |
And here is a branch rewriting While |
As an example of the impact that this change might have on a larger codebase, here is the Linkerd 2 proxy rewritten to use my I'll try to write up some of my thoughts on the experience and post them here as well. |
Here are some of my experiences thus far:
I don't feel like any of these issues are necessarily significant enough for me to come down strongly against making this change. However, I do feel like making this change has some impacts on ergonomics that ought to be taken into account when deciding whether or not to move forwards with it. |
Just from watching this unfold, while it does seem like from a purity point-of-view, the request should be a generic, in practice it seems like it is better to be an associated type. |
@seanmonstar That's roughly how I feel about it as well. At first glance, it certainly seems like the Right Thing, but it feels like it makes a lot of fairly common patterns with |
@seanmonstar could you dig in more detail. I’d like to hear more specifically what you consider the factors that tilt it towards keeping it as an associated type. We haven’t hit significant problems with the associated type yet, but I expect that the associated type will cause problems with reusable stack definitions once we get there. I will try to sketch out what I mean ASAP. |
And here's Making the change wasn't too difficult, but I think the resulting APIs may be a bit less easy to use (and/or uglier). |
I want to push back on this in one way: the practical experience here is with code that was all originally written with an associated type. Patterns that emerge from that definition of That is, rewriting existing code will tend to favor the old API, but writing new code might not have the same experience. |
@withoutboats it would be helpful if you have any thoughts as to new patterns :) |
That's true, and I'll freely admit that when I've been making this change in existing codebases, I've tried to intentionally make the smallest necessary change to get it to compile, rather than rewriting it from scratch. That said, I'm somewhat wary of weighing things that work now against potential future patterns --- I'm much more comfortable with comparing the drawbacks of the change with the patterns that we already know it would enable... |
@carllerche I don't have particular examples but vaguely, let's say you want to take any request that implements a certain trait: impl<R: MyTrait> Service<R> for MyService { } To do this with the assoc type API, you need to litter PhantomData: struct MyService<R> {
_request: PhantomData<R>,
...
}
impl<R: MyTrait> Service for MyService<R> { ... } This applies equally well if you want to write a service over a defined set of concrete types, e.g. There's an inverse relationship between the painpoints: if you want your service to be generic in the old API, you need to add PhantomData, whereas if you want to be generic over Services in the new API, you need to add PhantomData. |
It's possible also there's a bridge that makes this easier by implementing this trait for your services that are not generic trait SpecificService: Service<Self::Request> {
type Request;
} I think there's no way to provide a blanket impl of this, but if you are okay with expecting non-generic services to write this impl, I think you can then just write |
@hawkw regarding the new fn bound, can this be solved by adding a where clause on the new fn instead of the impl? |
@hawkw re “bridging the gap”
I think we just wouldn’t try to do that. Aliases would have to have a generic as well. How do things look is you go that route. |
@hawkw re ‘’ the linked example shouldn’t actually need that syntax? In general, I agree it is even more painful to do, but I believe that T as Trait should be minimized in general. Can it be completely avoided in the various codebases you updated? |
Yes, that does work. It's a little less ergonomic in cases where there's several inherent functions on the middleware struct and they all need the
I think we're on the same page here -- sorry if my earlier post wasn't clear about that. What I was saying was that while it's possible to implement a trait with associated types for a generic type, it's not really possible to implement a generic trait for another trait with associated types. So, I didn't do this in any of my branches --- aliases must always be generic as well, as you said.
I think almost all of the cases of I'm not sure how much of the |
@hawkw Would you be able to take your branches and submit them as PRs to your forked repo, that would make discussing the specific changes easier. |
@carllerche I've opened the following PRs: |
I believe that tower-rs/tower-grpc#64 is a pretty strong argument for making this change. |
I've opened hawkw/tower-web#1 for the change to |
@seanmonstar any updated thoughts? I'm thinking we should do this and move forward with the change sooner than later. |
Yea, seems good. +1 |
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Problem
Currently,
Service
represents the request as an associated type. This was originally done to mirrorSink
in thefutures
crate (relevant issue). However, requests are an "input" toService
which can make representing some things a bit tricky.For example, HTTP services accept requests in the form of
http::Request<T: BufStream>
. In order to implement a service that handles these requests, the service type must have a phantom generic:This, in turn, can cause type inference issues as the request body must be picked when
MyService
is initialized.Proposal
The
Service
trait would become:This moves the request type to a generic.
Impact
The full impact of this change is unknown. My best guess is it will be mostly ergonomic. For example:
Before:
After:
And it linearly gets worse on the number of generic services:
Before:
After:
today there already are plenty of cases in which the one has to add additional generic parameters. In order to bound an HTTP service today, the following is required:
In practice, this turns out to be quite painful and it gets unmanageable quickly. To combat this, "alias" traits are created. It is unclear how this change would impact the ability to use this trick.
Evaluation
In order to evaluate the impact of the proposed change, the change has been applied to Tower, and a number of projects that use Tower.
tower
tower-h2
tower-grpc
tower-web
linkerd2-proxy
The text was updated successfully, but these errors were encountered: