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

POC: It is possible to define callbacks without using macros if you use a helper trait #94

Closed
wants to merge 6 commits into from

Conversation

alsuren
Copy link
Contributor

@alsuren alsuren commented Jul 3, 2020

There is a discussion in the gotham gitter around doing a similar thing for route handling: https://gitter.im/gotham-rs/gotham?at=5ef9c6f4405be935cdcc4b7b . I thought I should share our approach here in case you are interested.

https://gist.github.com/87ab93979d770314e6698a9867d1e7e5 is probably worth reading first

GooseTaskCallback<'a> is equivalent to AsyncCallback<'a> in that gist.

There is a bunch of ugliness lying around (like the fact that I had to wrap it in Arc, because fn implements Clone but Fn does not). I suspect that there is a way around this, because we don't need this in Gotham.

Do you want me to keep pulling on this thread, or are you okay with your current macro-based approach?

@LionsAd
Copy link
Collaborator

LionsAd commented Jul 3, 2020

Would this allow us to pass in a closure as well?

Right now with the macro approach we had the problem that passing in a closure is not supported.

But a closure would be perfect to pass in dynamic paths.

Copy link
Contributor Author

@alsuren alsuren left a comment

Choose a reason for hiding this comment

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

I'm afraid this is another hit and run proof of concept from me. I probably won't have time to work on it again until next weekend (11th).

Have a play and see if it does what you want. I would also be interested in hearing feedback about whether the compiler errors you get are any good. If there are any error messages that could be improved with some hints, please post them here, and I might have a go at fixing them upstream.

Also I think that it might be possible to store GooseTask.function as a for<'a> Arc<dyn GooseTaskCallback<'a>> or something. I just haven't had more than half an hour to myself all week, so haven't been able to try it.

You did a really good job of simplifying my original async prototype. I'm looking forward to seeing what you can do with this one.

impl<'a, Fut: 'a, F> GooseTaskCallback<'a> for F
where
F: FnOnce(&'a GooseUser) -> Fut,
Fut: std::future::Future<Output = ()> + Send,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would this allow us to pass in a closure as well?

Yes. Closures implement FnOnce, which is what this blanket impl covers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

... actually I tried it and it turns out it doesn't get the lifetimes right. That's sad.

Copy link
Member

Choose a reason for hiding this comment

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

@alsuren Any ideas on how we could make this work with closures? That's the big inspiration for any further changes to how wrapping tasks works, to make Goose far more useful as a library.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm afraid I've not had the head space for this after work, so I've not looked into it.

Next things I would try would probably be:

  • See if there is a way to annotate the lifetimes on the closure as you create it
  • Take a step back to the previous approach (which managed to use closures as part of the workflow just fine, but couldn't accept them as a macro argument because we only accept ident as input) and see if there is a way to accept a closure as a macro argument. (Prototype this without using macros first maybe?)

@jeremyandrews
Copy link
Member

Very interesting, thanks! I'm currently traveling, but will dig into this when I return in a little over a week.

It looks like the only "error" is a clippy warning about complexity recommending we instead declare a type.

Do you want me to keep pulling on this thread, or are you okay with your current macro-based approach?

We welcome it if you want to keep pulling on this thread! :) As it could solve a problem we've run into with passing in a closure in place of a function, we'll likely also continue to dig in as well.

@jeremyandrews
Copy link
Member

Our primary interest in this was supporting closures, but @LionsAd got this working in #120 which landed today (and still uses macros).

I did run some performance comparisons on this PR, compared to the original implementation, compared to our newly added support for closures, and all are essentially the same:

What would the advantage of this PR be at this point? Quicker compile times?

@alsuren
Copy link
Contributor Author

alsuren commented Jul 29, 2020

Looks like #120 has incorporated the key interesting ideas behind this POC. Nice work. Seems reasonable to close this pr.

@alsuren alsuren closed this Jul 29, 2020
@LionsAd
Copy link
Collaborator

LionsAd commented Jul 29, 2020

I might still pursue this to create a more generic approach of capturing a function name into a closure and to make the macro just do:

GooseTask::from_function(my_function);

but yeah I will create a new PR for that :).

@LionsAd LionsAd reopened this Jul 29, 2020
@LionsAd
Copy link
Collaborator

LionsAd commented Jul 29, 2020

I am re-opening this - as it's easiest to communicate!

I did it - I was able to create a generic helper for this:

// Generic implementation
// -----

trait AsyncCallback<'a, U, T> {
    type Output: std::future::Future<Output=T> + 'a + Send;
    fn call(self, arg: &'a U) -> Self::Output;
}

impl<'a, Fut: 'a, F, U: 'a, T> AsyncCallback<'a, U, T> for F
where
    F: FnOnce(&'a U) -> Fut,
    Fut: std::future::Future<Output=T> + Send,
{
    type Output = Fut;
    fn call(self, arg: &'a U) -> Fut {
        self(arg)
    }
}

type AsyncTaskFunction<U,T> = Arc<dyn for<'r> Fn(&'r U) -> Pin<Box<dyn Future<Output = T> + Send + 'r>> + Sync + Send>;

fn from_async_function<F, U, T>(cb: F) -> AsyncTaskFunction<U, T>
where
    for<'a> F: AsyncCallback<'a, U, T> + Clone + 'static + Send + Sync,
{
        Arc::new(move |s| Box::pin(cb.clone().call(s)))
}

This makes it dead-simple to create new async task function types:

type GooseTaskFunction = AsyncTaskFunction<MyStruct, ()>;

And to wrap a function is as simple as:

    let wrapped_function = from_async_function(my_function);

Full playground:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b147cbd0d62d256e373ff8b2fd862689

@LionsAd
Copy link
Collaborator

LionsAd commented Jul 29, 2020

@alsuren
Copy link
Contributor Author

alsuren commented Jul 29, 2020

I will admit that I am a bit lost.

GooseTask::from_function(my_function) is basically what I was aiming for in this PR.

As was pointed out, this doesn't really give you anything over what you get from a macro, unless you can also have a constructor that accepts a closure.

It may even be possible to make the macro accept an arbitrary expression (rather than just an ident), and handle closures that way. I've not tried.

@LionsAd
Copy link
Collaborator

LionsAd commented Jul 30, 2020

Well - my goal was to make handling of async fn as simple as normal fns.

And the generic approach seems to work well.

Wrapping an async fn is now possible without even knowing it’s type.

Eg what I have written could go into a helper crate.

I consider that a win.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants