Builder loosing implementation

The problem

When I use .predicate() with a closure, RequireBuilder loses its build implementation.
It works fine with structs that implement PredicateAsync, but not with a closure or a function.
.fallback(), which is quite similar, works fine.

Generics Pr2 and Pr require the same implementations, so I dunno why would it loose its implementation.

here is this error quickly recreated in Rust Playground by a GPT.

        let f = |backend, user, state| verify_permissions(backend, user, state);
        let require_login = Require::<TestBackend, TestState>::builder_with_state(state.clone())
            .fallback(RedirectFallback::new().login_url("/login"))
            .predicate(f) 
            .build();

compilation error:

error[E0599]: no method named `build` found for struct `RequireBuilder<TestBackend, TestState, Body, ..., ..., ...>` in the current scope
   --> axum-login/src/require/builder/tests.rs:796:14
    |
792 |           let require_login = Require::<TestBackend, TestState>::builder_with_state(state.clone())
    |  _____________________________-
793 | |             .fallback(RedirectFallback::new().login_url("/login"))
794 | |             .restrict(|_| async { StatusCode::UNAUTHORIZED.into_response() })
795 | |             .predicate(f)
796 | |             .build();
    | |             -^^^^^ method not found in `RequireBuilder<TestBackend, TestState, Body, ..., ..., ...>`
    | |_____________|
    |
    |
   ::: axum-login/src/require/builder/mod.rs:29:1
    |
29  | / pub struct RequireBuilder<
30  | |     B,
31  | |     ST = (),
32  | |     T = Body,
...   |
35  | |     Pr = DefaultPredicate<B, ST>,
36  | | > {
    | |_- method `build` not found for this struct
    |
    = note: the method was found for
            - `RequireBuilder<B, ST, T, Fb, Rs, Pr>`

RequireBuilder implementation

impl<B, ST, T> RequireBuilder<B, ST, T, DefaultFallback, DefaultRestrict, DefaultPredicate<B, ST>>
where
    DefaultPredicate<B, ST>: PredicateAsync<B, ST>,
    B: AuthnBackend,
    ST: std::marker::Send + std::marker::Sync + std::clone::Clone,
    T: 'static + Send,
{
    /// Creates a new `RequireBuilder` with a set state.
    pub fn new_with_state(state: ST) -> Self {
        Self {
            predicate: DefaultPredicate {
                _backend: PhantomData,
                _state: PhantomData,
            },
            restrict: DefaultRestrict,
            fallback: DefaultFallback,
            state,
            _backend: PhantomData,
            _body: PhantomData,
        }
    }
}

impl<B, ST, T, Fb, Rs, Pr> RequireBuilder<B, ST, T, Fb, Rs, Pr>
where
    B: AuthnBackend,
    T: 'static + Send,
    ST: Clone + std::marker::Send,
    Fb: AsyncFallbackHandler<T> + std::marker::Send + std::marker::Sync,
    Rs: AsyncFallbackHandler<T> + std::marker::Send + std::marker::Sync,
    Pr: PredicateAsync<B, ST> + std::marker::Send + std::marker::Sync,
{
    pub fn predicate<Pr2>(self, new_predicate: Pr2) -> RequireBuilder<B, ST, T, Fb, Rs, Pr2>
    where
        Pr2: PredicateAsync<B, ST> + std::marker::Send + std::marker::Sync,
    {
        RequireBuilder {
            predicate: new_predicate,
            restrict: self.restrict,
            fallback: self.fallback,
            state: self.state,
            _backend: PhantomData,
            _body: PhantomData,
        }
    }

    pub fn fallback<Fb2>(self, new_fallback: Fb2) -> RequireBuilder<B, ST, T, Fb2, Rs, Pr>
    where
        Fb2: AsyncFallbackHandler<T> + std::marker::Send + std::marker::Sync,
    {
        RequireBuilder {
            predicate: self.predicate,
            restrict: self.restrict,
            fallback: new_fallback,
            state: self.state,
            _backend: PhantomData,
            _body: PhantomData,
        }
    }

PredicateAsync implementation

impl<F, Fut, B, ST> PredicateAsync<B, ST> for F
where
    F: Fn(&B, &<B as AuthnBackend>::User, &ST) -> Fut,
    Fut: Future<Output = bool>,
    B: AuthnBackend + AuthzBackend + 'static,
    B::User: 'static,
    B::Permission: Clone + Debug,
    ST: Clone + Send + Sync + 'static,
{
    type Future = Fut;

    fn predicate(
        & self,
        backend: &B,
        user: &<B as AuthnBackend>::User,
        state: &ST,
    ) -> Self::Future {
        (self)(backend, user, state)
    }
}
pub trait PredicateAsync<B: AuthnBackend, ST = ()> {
    type Future: Future<Output = bool>;
    fn predicate(
        & self,
        backend: &B,
        user: &<B as AuthnBackend>::User,
        state: &ST,
    ) -> Self::Future;
}

Full context

this blanket impl block implements the trait PredicateAsync for closure and function
types with late bound lifetimes for the argument types and fixed return type (the Fut), but the desugared function type of async fn always captures the lifetime of the parameter types, even if the arguments are not stored across await points.

on the other hand, you cannot specify the precise captures for the return type of Fn() trait when the argument has late bound lifetimes (unless you use the unstable unboxed_closures feature.

so, it's a dilemma really, and I don't how, if possible at all, to work around these limitations. maybe you can design the PredicateAsync trait based on the newer AsyncFn trait, but it has other drawbacks.

1 Like

here's a reduced example demonstrating the late bound lifetime problem:

and here's a PoC to work around the issue by replacing the named associated type Future with impl Future and precise captures, plus desugared Fn trait:

I'm pretty sure there exist simpler solutions, possibly on stable, but I don't have much time on this for now.

2 Likes

Thank you very much for your response.
It seems I’m stuck between a rock and a hard place and I’m determined to get to the bottom of it.
I honestly had never heard about precise capture and unsafe features like unboxed_closures. Where can I read more about this and similar obscure things? RFCs?

While, as I understand it, the RFC text isn't 100% required to align with what's implemented, the Precise capturing RFC should be a good source: 3617-precise-capturing - The Rust RFC Book

The edition guide may have a better high level view:


The unstable book entry for unboxed_closures doesn't seem relevant to the workaround provided up thread, more that it allows you to reference Fn<...> and therefore Fn<...>::Output.
The compiler was helpful in showing me this, if I remove the feature flag from what was posted.

the main difference between the two playground links is I changed the AsyncPredicate to use return position impl trait in trait, instead of using a named associated type Future and returning Self::Future. this is what makes the code work.

the precise capture list there is just I was being extra explicit, it is not necessary.

the unboxed_closures is there because I need to add the Future bound on the associated type Fn<...>::Output without using an equality constraint, which is not possible with the parenthesis Fn(...) -> Output syntax. this is actually possible with a helper trait without unstable features too, it's not very complex, but I want the example to be concise so I just used nightly.

now, let me try to explain the problem with some other examples, hopefully to demonstrate the problem with HRTB using parenthesis Fn(Arg) -> Ret syntax.

suppose I have this function:

fn call_with_secret<F, R>(f: F) where F: FnOnce(&i32) -> R {
    let secret = 42;
    f(&secret);
}

the idea is simple: I want to call any function that can accept an &i32 as argument, and I don't care the return type, I just ignore the return value.

now let's define some functions and call them:

fn return_unit(_: &i32) {}
fn return_boolean(x: &i32) -> bool { (*x) % 2 == 0 }
fn return_i32(x: &i32) -> i32 { (*x) + 1 }

call_with_secret(return_unit);
call_with_secret(return_boolean);
call_with_secret(return_i32);

seems to be working as expected! but wait, what happened to this one:

fn return_ref(x: &i32) -> &i32 { x }

call_with_secret(return_ref);

we got an compile error:

error[E0308]: mismatched types
  --> src/main.rs:5:2
   |
5  |     call_with_secret(return_ref);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected reference `&_`
              found reference `&_`

what does that mean? expected &_, found &_?

ok, what is the type of return_ref? it's something like: F: for<'a> FnOnce(&'a i32) -> &'a i32. do you see the problem? the trait bound in our call_with_secret requires F: for<'a> FnOnce(&'a i32) -> R, can't we just infer R to be &'a i32?

well, no, because R must be a single type, but &'a i32 is a family of infinitely many types, each with different lifetime 'a, and this lifetime 'a is depending on the argument type.

but I just want to ignore the return value, I don't care about the type! as long as it can take an argument of type &i32, I'm happy to call it. but the parenthesis Fn(Arg) -> Ret syntax forces me to give a single name to the return type, even if I want to ignore it.

how to solve it? in theory, there's two possible solutions:

  • if I can declare the return type R doesn't need to be a single type, it can depend on the argument type, i.e. I want to write the signature like this:

    fn call_with_secret_hkt<F, R<'_>>(f: F) where F: for<'a> FnOnce(&'a i32) -> R<'a>;
    

    unfortunately, this is not supported by rust.

  • how about I don't mention the return type, I just want to limit the argument type to &i32?

    it turns out the return type of the FnOnce trait is an associated type Output, if I use the angular bracket syntax FnOnce<Args> instead of the parenthesis syntax FnOnce(Args), indeed I don't need to mention the return type:

    #![feature(unboxed_closures)]
    fn call_with_secret_unstable<F>(f: F) where F: for<'a> FnOnce<(&'a i32,)>;
    

let's try it out:

call_with_secret_unstable(return_unit);
call_with_secret_unstable(return_boolean);
call_with_secret_unstable(return_i32);
call_with_secret_unstable(return_ref);

and it indeed works.

but how about async functions? first, let's define some async functions:

async fn return_future_unit(_: &i32) {}
async fn return_future_boolean(x: &i32) -> bool { (*x) % 2 == 0 }
async fn return_future_ref(x: &i32) -> &i32 { x }

if I call them use the old call_with_secret:

call_with_secret(return_future_unit);
call_with_secret(return_future_boolean);
call_with_secret(return_future_ref);

I got the same compile error:

error[E0308]: mismatched types
  --> src/main.rs:8:2
   |
8  |     call_with_secret(return_future_unit);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected opaque type `impl for<'a> Future<Output = ()>`
              found opaque type `impl Future<Output = ()>`

but if I call them with the new call_with_secret_unstable, it compiles!

call_with_secret_unstable(return_future_unit);
call_with_secret_unstable(return_future_boolean);
call_with_secret_unstable(return_future_ref);

as we saw, the (desugared) return type of an async function always captures the lifetimes of the arguments.

in other words, if an async function's parameter types contain lifetimes, the opaque return type (i.e. the Future) MUST be a generic type over lifetimes. it is a family of Future typess, it is NOT a single Future type.

consequently, if you have a trait with an associated Future type (such as the AsyncPredicate trait in OP), you will NOT be able to implement it for async functions where the function argument types contain lifetimes, because the opaque return type of such async function is not a single Future.

return position impl trait in trait works, because you don't need to define a single associated Future type, but the compiler will generate it (which I believe is a GAT) for you automatically.

theoretically, you may be able to make it work if you make the associated Future a GAT AsyncPredicate::Future<'a>, or if you make the trait itself generic over lifetime AsyncPredicate<'a>, there's also the possibility to use the AsyncFn(&Arg)->bool trait (as opposed to Fn(&Arg)->Fut) for the blanket implementation, but I didn't bother to explore them.

3 Likes

Again thank you for the explanation, I think I get it now.

I have already tried implementing AsyncFn, unfortunately without success.

I will test a few other implementations I have, but since it will take unknown amount time I will select your explanation as the answer and will add my own later, if I succeed ofc.

This seems to work. Although without associated types I have a problem with my custom futures.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.