Skip to content
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

Add Future-like combinators #396

Merged
merged 24 commits into from
Jul 17, 2020
Merged

Conversation

hlb8122
Copy link
Contributor

@hlb8122 hlb8122 commented Dec 17, 2019

Motivation
It is often the case that one wishes to take a service and modify it's output or input in some way without changing the behavior of poll_ready(). For example, turning a service returning Result<(Parts, Body), _> into a service returning Result<Request<Body>, _> or modifying the error type.

Currently, there is no ergonomic way to apply such changes without a fair amount of boilerplate.

Proposal
Extend ServiceExt trait to add various methods mirroring the combinators from TryFutureExt and various services and layers to accommodate this.

Requirements
Because combinators could be heavily chained together it's probably wise to trim all the fat off here and if possible make it zero-cost when compared to simply applying the combinators inside of the inner service's call.

Result
Having such methods should allow a more expressive algebra of tools to construct and combine services.

Current Implementation
At the time of writing I've introduced four combinators:

    fn map_ok<F, Response>(self, f: F) -> MapOk<Self, F>
    where
        Self: Sized,
        F: FnOnce(Self::Response) -> Response + Clone,
    {
        MapOk::new(self, f)
    }

    fn map_err<F, Error>(self, f: F) -> MapErr<Self, F>
    where
        Self: Sized,
        F: FnOnce(Self::Error) -> Error + Clone,
    {
        MapErr::new(self, f)
    }

    fn with<F, NewRequest>(self, f: F) -> With<Self, F>
    where
        Self: Sized,
        F: FnOnce(NewRequest) -> Request + Clone,
    {
        With::new(self, f)
    }

    fn try_with<F, NewRequest>(self, f: F) -> With<Self, F>
    where
        Self: Sized,
        F: FnOnce(NewRequest) -> Result<Request, Self::Error> + Clone,
    {
        TryWith::new(self, f)
    }

and an associated Service and Layer for each.

TODO

  • Which trait constraints are minimally required on the closures to ensure an ergonomic API without any excess overhead?
  • Documentation
  • More combinators such as and_then

@hlb8122 hlb8122 changed the title (WIP) Add rudimentary combinators (WIP) Add Future-like combinators Dec 17, 2019
tower-util/src/lib.rs Outdated Show resolved Hide resolved
@LucioFranco
Copy link
Member

This looks like a fantastic start! I would like to review this once we get the 0.3 release out since we can always add more items in follow up patches.

@jonhoo jonhoo requested a review from LucioFranco March 31, 2020 22:21
@jonhoo jonhoo added A-util Area: The tower "util" module C-enhancement Category: A PR with an enhancement or a proposed on in an issue. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Mar 31, 2020
@hlb8122 hlb8122 marked this pull request as draft April 15, 2020 19:48
@hlb8122
Copy link
Contributor Author

hlb8122 commented Jun 26, 2020

Has there been any decisions made with respect to this? I noticed map_request has been implemented in #435.

@hlb8122 hlb8122 changed the title (WIP) Add Future-like combinators Add Future-like combinators Jun 26, 2020
Copy link
Member

@hawkw hawkw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this, I'd definitely like to get this in!

I had some suggestions, including a few style nits & API design questions to think about.

Also, I think a lot of this code has moved around on master --- would you mind rebasing and moving the new code to track that? I don't think there are any other differences that should effect this PR besides code moving around.

tower-util/src/combinators/map_err.rs Outdated Show resolved Hide resolved
tower-util/src/combinators/map_err.rs Outdated Show resolved Hide resolved
Comment on lines 22 to 23
F: FnOnce(S::Error) -> Error,
F: Clone,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using FnOnce + Clone here seemed a little weird at first, but then I realized that the requirement that the function be moved into the MapErr future and cloned in Layer::layer kind of drives this. So if we're going to have to require Clone, it makes sense to use FnOnce.

use tower_layer::Layer;
use tower_service::Service;

pub struct With<S, F> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tower-http contains a tower-request-modifier crate that does a bunch of HTTP-specific request mutations...I wonder if that could be reimplemented on top of this API, eventually? It would be nice to keep that in mind and make sure this is possible.

Copy link
Contributor Author

@hlb8122 hlb8122 Jun 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll give it a fork and try it out when I get some spare time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tower-rs/tower-http#31

I departed from the previous API by a fair bit but I think it turned out ok.

tower-util/src/combinators/with.rs Outdated Show resolved Hide resolved
tower-util/src/combinators/with.rs Outdated Show resolved Hide resolved
tower-util/src/lib.rs Outdated Show resolved Hide resolved
tower-util/src/lib.rs Outdated Show resolved Hide resolved
tower-util/src/lib.rs Outdated Show resolved Hide resolved
tower-util/src/lib.rs Outdated Show resolved Hide resolved
Copy link
Member

@hawkw hawkw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really good to me!

I noticed a couple things that would be good to address:

  • CI will warn us unless we add doc comments to all the new public types introduced here --- let's do that. Since the main way users will interact with these types will be via the combinators, it's okay if the types' docs are pretty minimal, as long as they link to the combinator that creates them. This is essentially what std's docs do for iterator combinators etc.
  • Looks like the examples don't actually compile on CI --- let's fix that, please!

Once those issues are addressed, I would be happy to merge this, unless @LucioFranco or @jonhoo have any thoughts on naming.

tower/src/util/combinators/map_err.rs Outdated Show resolved Hide resolved
tower/src/util/combinators/map_err.rs Outdated Show resolved Hide resolved
tower/src/util/combinators/map_err.rs Outdated Show resolved Hide resolved
tower/src/util/combinators/map_err.rs Outdated Show resolved Hide resolved
tower/src/util/combinators/map_ok.rs Outdated Show resolved Hide resolved
tower/src/util/mod.rs Outdated Show resolved Hide resolved
tower/src/util/mod.rs Outdated Show resolved Hide resolved
tower/src/util/mod.rs Outdated Show resolved Hide resolved
tower/src/util/mod.rs Outdated Show resolved Hide resolved
tower/src/util/mod.rs Outdated Show resolved Hide resolved
@hlb8122
Copy link
Contributor Author

hlb8122 commented Jun 30, 2020

Quick note about the documentation: I've used intra-doc links here which are only supported on nightly (and the docs.rs compiler).

The reason for this is that the of #[doc(inline)] on ServiceExt in lib.rs breaks the relative links.

The same problem exists for the CallAll link in the documentation of ServiceExt::call_all method from the /doc/tower/trait.ServiceExt.html page (rather than /doc/tower/util/trait.ServiceExt.html)

@hlb8122 hlb8122 marked this pull request as ready for review June 30, 2020 04:32
Copy link
Member

@LucioFranco LucioFranco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super sorry for the delay on reviewing this!!!!!

Overall, looks fantastic, I have a couple comments inline. Let me know if you have any questions. The Map layer that exists already is probably a good reference point and we should try and match that.

impl<S, F, Request, Error> Service<Request> for MapErr<S, F>
where
S: Service<Request>,
F: FnOnce(S::Error) -> Error + Clone,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why clone? Why not Fn(S::Error) -> Error? I believe Fn is semantically similar to FnOnce + Clone while being less restrictive.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LucioFranco I don't think that's the case. Fn can be called with an & reference. However, since the function is called after the inner service's future, the function needs to be moved into the future that the MapErr service returns. Therefore, it cannot be borrowed without constraining the future's lifetime --- so it must be Cloned. And if it has to be Clone, it may as well be FnOnce, since each clone will only be called a single time.

If we wanted to use Fn, we would need to wrap it in an Arc internally, and clone the Arc into each future. That would also work, but in some cases (such as function pointers), the Arc isn't needed, since little-f fn can be freely cloned already.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hawkw is right, we should then change the bounds on the map combinators as well.

{
type Response = S::Response;
type Error = Error;
type Future = MapErrFut<S::Future, F>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been using MapErrFuture instead of just Fut. See the map combinator.

tower/src/util/combinators/map_err.rs Outdated Show resolved Hide resolved
tower/src/util/combinators/map_ok.rs Outdated Show resolved Hide resolved
{
type Response = Response;
type Error = S::Error;
type Future = MapOkFut<S::Future, F>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

tower/src/util/combinators/try_with.rs Outdated Show resolved Hide resolved
use tower_layer::Layer;
use tower_service::Service;

/// Service returned by the [`try_with`] combinator.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we get a bit more in the docs here? The name isn't clear enough that we probably want to add some more docs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also link to the docs in the extension trait.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should link to the combinator that creates it, which is the authoritative source for the docs. It's also what users will probably see first. Since the combinator docs are linked, I don't think the type really needs any additional documentation --- it would just add the burden of having to maintain two separate versions of essentially the same documentation, keep them in sync, and so on.

This is basically what std does for iterator combinators etc.

tower/src/util/combinators/try_with.rs Outdated Show resolved Hide resolved
use tower_layer::Layer;
use tower_service::Service;

/// Service returned by the [`with`] combinator.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

tower/src/util/combinators/with.rs Outdated Show resolved Hide resolved
Copy link
Member

@LucioFranco LucioFranco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@LucioFranco
Copy link
Member

Thanks @hlb8122 a ton!

tower/src/util/map_err.rs Show resolved Hide resolved
tower/src/util/map_err.rs Show resolved Hide resolved
tower/src/util/map_ok.rs Show resolved Hide resolved
tower/src/util/map_ok.rs Show resolved Hide resolved
tower/src/util/mod.rs Show resolved Hide resolved
tower/src/util/mod.rs Show resolved Hide resolved
/// Composes a function *in front of* the service.
///
/// This adapter produces a new service that passes each value through the
/// given function `f` before sending it to `self`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this should reference the futures::SinkExt::with method as a comparison? Not a blocker.

///
/// This adapter produces a new service that passes each value through the
/// given function `f` before sending it to `self`.
///
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, maybe worth linking futures::SinkExt::try_with here? Not a big deal.

tower/src/util/try_with.rs Show resolved Hide resolved
tower-util/src/lib.rs Outdated Show resolved Hide resolved
Copy link
Member

@hawkw hawkw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me --- only a handful of docs issues.

I'm not sure if the links without a reference work correctly or not. It might be worth running cargo docs --open -p tower and making sure that all the links are rendered correctly and don't 404 when clicked on.

If I'm wrong, and RustDoc can handle this correctly, please disregard my comments.

tower/src/util/try_with.rs Show resolved Hide resolved
tower/src/util/with.rs Show resolved Hide resolved
tower/src/util/with.rs Show resolved Hide resolved
tower/src/util/with.rs Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-util Area: The tower "util" module C-enhancement Category: A PR with an enhancement or a proposed on in an issue. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants