Associated type and FnOnce bound problem

I am having difficulty understanding what compiler wants from me in this snippet:

#![allow(dead_code)]
#![allow(unused_variables)]
#![allow(unused_imports)]
use tokio::task::spawn_local;
use futures::Future;


// define closure that returns future with same lifetime as it's single parameter
trait AsyncCB1<'a, A: 'a, O>: FnOnce(A) -> Self::Fut {
    type Fut: Future<Output = O>;
}


impl<'a, A:'a, O, Out, F> AsyncCB1<'a, A, O> for F
where
    Out: Future<Output = O>,
    F: FnOnce(A) -> Out,
{
    type Fut = Out;
}


// wrap_task takes closure (that takes &impl TaskCtx) and awaits on produced future
trait TaskCtx {}


// async fn wrap_task<FN, C: TaskCtx, T>(ctx: C, fn_task: FN)
// where
//     FN: 'static + for<'a> AsyncCB1<'a, &'a C, T>,
// {
//     let inner = fn_task(&ctx);
//     inner.await;
// }


// TaskCreator wraps and spawns task created by closure
trait TaskCreator {
    type TaskCtx: TaskCtx;

    fn spawn_task<FN, T>(self, fn_task: FN)
    where
        FN: 'static + for<'a> AsyncCB1<'a, &'a Self::TaskCtx, T>;
}

struct TaskCreatorImpl;

impl TaskCreator for TaskCreatorImpl {
    type TaskCtx = TaskCtxImpl;

    fn spawn_task<FN, T>(self, fn_task: FN)
    where
        FN: 'static + for<'a> AsyncCB1<'a, &'a Self::TaskCtx, T>
    {
        // spawn_local(wrap_task(ctx, fn_task));
    }
}

struct TaskCtxImpl;
impl TaskCtx for TaskCtxImpl {}


// foo() takes impl TaskCreator and uses it to wrap and spawn it's test task
// struct Test;
// impl Test {
//     async fn test(self, _ctx: &impl TaskCtx) {}
// }


// async fn run_test(ctx: &impl TaskCtx) {
//     Test.test(ctx).await;
// }

// async fn foo(creator: impl TaskCreator) {
//     creator.spawn_task(TaskCtxImpl, run_test)
// }

Can you help me understand what is wrong and how to get this code to work?

Thank you.

It seems that you are dealing with an unnameable closure type. Afaik you can only deal with this with type erasure (a.k.a. dyn Trait), but I would wait to see if someone else has a more concrete answer.

No, this has nothing to do with unnameable types.

The problem is the supertrait bound which is too eager. If you remove it, it compiles. (I also simplified the lifetimes a bit using GATs.)

Now, of course, the supertrait bound is convenient for the implementor. An (arguably) nicer (?) solution would be to keep the supertrait bound and add the necessary constraints to the signature of TaskCreator::spawn_task().

3 Likes

The normalization issue is probably this issue or related.

Incidentally I don't think the 'a parameter on the callback trait gains you anything.

2 Likes

Removing supertrait bound causes FN to stop being a FnOnce and wrap_task can't invoke it anymore (playground). So, this doesn't really help me... :frowning:

True, I didn't really know what I was doing. See updated code in playground link above -- I still can't get it to compile. :-/

Note: this code is a significantly reduced version of real-world code where wrap_task is doing some extra work in parallel to inner (via select!) and (once inner is finished) needs to use ctx. Right now that code make a clone of ctx (and fn_inner takes ctx by value), I tried to change fn_inner to take &ctx to avoid somewhat expensive clone. But I didn't expect to open a huge can of worm and spend 4 days on this seemingly trivial thing.

@c.m This might be interesting for you. :slight_smile:

EDIT: Look here.

Doesn't really help me -- this forces me to discard one level of indirection (hiding TaskCtxImpl behind impl TaskCtx). Real TaskCtxImpl and TaskCreatorImpl have bunch of type parameters (with some extra bounds) and using FN: for<'a> AsyncCB1<&'a TaskCtxImpl<...>, T> instead of FN: for<'a> AsyncCB1<&'a TaskCtx, T> causes these parameters (and bounds) to spill into client code (foo and test functions) defeating the whole purpose of aforementioned indirection and turning client's code into hell.

You could play with this here, if you want -- I added just one generic parameter to these structs and it is already a major PITA.

Yes, sure. The example I posted was not intended as a solution to your specific problem, but to show that there is probably a problem in rustc that appears to be caused by refering to the unnormalized AT (Self::TaskCtx).^^

@keks993 is there a workaround? Maybe a syntax to "normalize" AT?

Hmm... idk might something like this be a workaround for what you try to achieve?

Another possible workaround (moving the associated typed to be a type parameter of the trait).

2 Likes

This works -- client side (foo and test functions) got a bit longer, but I could live with that. Thank you!

Nope, not gonna cut it :). Thankfully, @quinedot proposed a good workaround.

This was quite a ride... Tried to pass one value by reference -- ended up stepping on three mines (async fn can't meet FnOnce bound, closure issue and normalization problem). All problems are way too advanced for my current level, unfortunately. Thank you for your help again, @quinedot .

2 Likes

Edit: nevermind, using trait Task { type R; async fn run(self, ctx: &impl TaskCtx) -> Self::R; } makes everything simple (and I don't need to deal FnOnce problems). :slight_smile:

@quinedot ... and I ran into another problem -- fn_task that gets passed into spawn_task needs to have a state. In previous code snippets I used fn to avoid the "closure issue" and assumed that in real-life code (where task struct arrives from outside) I'll just wipe up a manual implementation of a closure. Smth like this:

trait Runnable<C, R> {
    async fn run(self, ctx: C) -> R;
}

struct MyClosure<S, R> {
    state: S,
}

impl<S, R, C, Fut> FnOnce(&C) -> Fut for MyClosure<S, R>
where
    Fut: Future<Output = R>,
    S: for<'a> Runnable<&'a C, R>
{
    fn call_once(self, args: (&C,)) -> Self::Output {
        self.state.run(args.0)
    }
}

struct Task;
struct TaskCtx;

impl Runnable<&TaskCtx, u32> for Task {
    async fn run(self, ctx: &TaskCtx) -> u32 { 42 }
}

Unfortunately, manual implementation of FnOnce is experimental and even when enabled -- doesn't allow associated types (compilation fails). :frowning: I guess another approach to this is to get rid of FnOnce completely and implement fn_task invocation manually via trait.

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.