Trait with complicated associated types and bounds

Although I have minimized it as much as possible, my problem is quite intricate. Playground at the bottom. I am going to tip you if you can solve the issue, because I am stuck.

Library

I am implementing a library for actors. It contains an ActorBounds trait with a spawn method.
This method takes a closure as argument, which it saves and returns in an ActorTask struct.
This struct has a run method that calls the inner closure.

These traits and structs have many types, whose purpose will subsequently become clear by means of an example.

pub trait ActorBounds<M> {
    type ChildActorBoundsType<M2>;
    type ChildActorBounds<M2>: ActorBounds<M2>;

    fn spawn<M2, F>(
        &self,
        f: F,
    ) -> ActorTask<M2, F, Self::ChildActorBoundsType<M2>, Self::ChildActorBounds<M2>>
    where
        F: FnOnce(Self::ChildActorBounds<M2>);
}

pub struct ActorTask<M, F, CABT, CAB> {
    m: PhantomData<M>,
    f: F,
    cell: ActorCell<M, CABT>,
    t: PhantomData<CAB>,
}

impl<M, F, CABT, CAB> ActorTask<M, F, CABT, CAB>
    where
        F: FnOnce(ActorCell<M, CABT>),
{
    fn run(self) {
        let f = self.f;
        let cell = self.cell;
        &cell.m; // must be able to access m field
        f(cell);
    }
}

The ActorBound trait is implemented by an ActorCell struct:

pub struct ActorCell<M, AB> {
    pub m: PhantomData<M>,
    pub t: AB,
}

There are the two distinct implementations:

Implementation 1
struct StandardBounds<M>(PhantomData<M>);

impl<M> ActorBounds<M> for ActorCell<M, StandardBounds<M>> {
    type ChildActorBoundsType<M2> = StandardBounds<M2>;
    type ChildActorBounds<M2> = ActorCell<M2, StandardBounds<M2>>;

    fn spawn<M2, F>(
        &self,
        f: F,
    ) -> ActorTask<M2, F, StandardBounds<M2>, ActorCell<M2, StandardBounds<M2>>>
    where
        F: FnOnce(ActorCell<M2, StandardBounds<M2>>),
    {
        let cell = ActorCell {
            m: PhantomData::<M2>,
            t: StandardBounds(PhantomData::<M2>),
        };
        ActorTask {
            m: PhantomData::<M2>,
            f,
            cell,
            t: PhantomData::<ActorCell<M2, StandardBounds<M2>>>,
        }
    }
}
Implementation 2
struct TestBounds<M>(PhantomData<M>);

impl<M> ActorBounds<M> for ActorCell<M, TestBounds<M>> {
    type ChildActorBoundsType<M2> = TestBounds<M2>;
    type ChildActorBounds<M2> = ActorCell<M2, TestBounds<M2>>;

    fn spawn<M2, F>(&self, f: F) -> ActorTask<M2, F, TestBounds<M2>, ActorCell<M2, TestBounds<M2>>>
    where
        F: FnOnce(ActorCell<M2, TestBounds<M2>>),
    {
        let cell = ActorCell {
            m: PhantomData::<M2>,
            t: TestBounds(PhantomData::<M2>),
        };
        ActorTask {
            m: PhantomData::<M2>,
            f,
            cell,
            t: PhantomData::<ActorCell<M2, TestBounds<M2>>>,
        }
    }
}

Example usage

The user of the library will use the ActorBounds trait as follows:

async fn takes_cell<AB>(mut cellu64: AB)
where
    AB: ActorBounds<u64>,
{
    let task = cellu64.spawn::<u32, _>(|cellu32| {
        let task = cellu32.spawn::<u16, _>(|cellu16| {
            let task = cellu16.spawn::<u8, _>(|cellu8| {
                //
            });
            task.run();
        });
        task.run();
    });
    task.run();
}

The user would call the function as follows. Notice how the two ActorCell variables have different types but they both work as they respect the prescribed bound:

fn main() {
    let cell = ActorCell {
        m: PhantomData::<u64>,
        t: StandardBounds(PhantomData::<u64>),
    };
    takes_cell(cell); // ActorCell<u64, StandardBounds<u64>>

    let cell = ActorCell {
        m: PhantomData::<u64>,
        t: TestBounds(PhantomData::<u64>),
    };
    takes_cell(cell); // ActorCell<u64, TestBounds<u64>>
}

Logical purpose of the types

Consider the first takes_cell call, where the user passes an ActorCell<u64, StandardBounds<u64> in.
Logically, the intended effect is as follows:

async fn takes_cell<AB>(mut cellu64: AB) // cellu64 = ActorCell<u64, StandardBounds<u64>
where
    AB: ActorBounds<u64>,
{
    let task = cellu64.spawn::<u32, _>(|cellu32| { // cellu32 = ActorCell<u32, StandardBounds<u32>
        let task = cellu32.spawn::<u16, _>(|cellu16| { // cellu16 = ActorCell<u16, StandardBounds<u16>
            let task = cellu16.spawn::<u8, _>(|cellu8| { // cellu8 = ActorCell<u8, StandardBounds<u8>
                //
            });
            task.run();
        });
        task.run();
    });
    task.run();
}

The initial cell has StandardBounds. All the cells spawned from it have StandardBounds too.

Error

Consider a simplified version of takes_cell where I spawn and run one cell only. It fails to compile due to unsatisfying trait bounds in run:

Simplified takes_cell
async fn takes_cell<AB>(mut cellu64: AB)
where
    AB: ActorBounds<u64>,
{
    let task = cellu64.spawn::<u32, _>(|cellu32| {
        //
    });
    task.run();
}
Compiler error
error[E0599]: the method `run` exists for struct `ActorTask<u32, {closure@main.rs:93:40}, <AB as ActorBounds<u64>>::ChildActorBoundsType<u32>, ...>`, but its trait bounds were not satisfied
   --> src/main.rs:102:10
    |
21  | pub struct ActorTask<M, F, CABT, CAB> {
    | ------------------------------------- method `run` not found for this struct
...
93  |     let task = cellu64.spawn::<u32, _>(|cellu32| {
    |                                        --------- doesn't satisfy `<_ as FnOnce<(ActorCell<u32, <AB as ActorBounds<u64>>::ChildActorBoundsType<u32>>,)>>::Output = ()` or `_: FnOnce<(ActorCell<u32, <AB as ActorBounds<u64>>::ChildActorBoundsType<u32>>,)>`
...
102 |     task.run();
    |          ^^^ method cannot be called due to unsatisfied trait bounds
    |
    = note: the full type name has been written to '/home/steddy/RustroverProjects/reproducers/target/debug/deps/reproducers-3be11c635dc30962.long-type-9967442127667851649.txt'
note: the following trait bounds were not satisfied:
      `<{closure@src/main.rs:93:40: 93:49} as FnOnce<(ActorCell<u32, <AB as ActorBounds<u64>>::ChildActorBoundsType<u32>>,)>>::Output = ()`
      `{closure@src/main.rs:93:40: 93:49}: FnOnce<(ActorCell<u32, <AB as ActorBounds<u64>>::ChildActorBoundsType<u32>>,)>`
   --> src/main.rs:30:8
    |
28  | impl<M, F, CABT, CAB> ActorTask<M, F, CABT, CAB>
    |                       --------------------------
29  | where
30  |     F: FnOnce(ActorCell<M, CABT>),
    |        ^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound introduced here

Requirements

How can I make the code compile respecting the following requirement?

:rotating_light: The ActorTask::run method must be able to access the m field in ActorCell. :rotating_light:

Ideally, takes_cell should remain unaltered, in the sense that the user does not have to type too many complicated additional trait bounds over AB.

My intuition: Is this perhaps achievable by modifying the bounds on the associated types in ActorBounds of the bounds of ActorBounds::spawn?

It seems like this...

    type ChildActorBounds<M2>: ActorBounds<M2>;

...should go away, since it's always...

ActorCell<M2, Self::ChildActorBoundsType<M2>>

...but if you try to require that type to satisfy the ActorBounds<_> bound, recursive bounds on the GAT defeats the compiler, so I guess that's why you have the GAT.

I tried tripling down on the abstractions instead. This implementation on your spawn return type has a lot of type parameters:

impl<M, F, CABT, CAB> ActorTask<M, F, CABT, CAB>
where
    F: FnOnce(ActorCell<M, CABT>),
{
    fn run(self) {

But in practice you can define the relevant ActorTask types in the ActorBounds implementations from two input parameters (here illustrated as a third GAT in the trait):

    type SpawnOut<M2, F> = ActorTask<M2, F, StandardBounds<M2>, ActorCell<M2, StandardBounds<M2>>>
    where
        F: FnOnce(ActorCell<M2, StandardBounds<M2>>),
    ;
    // ...

    type SpawnOut<M2, F> = ActorTask<M2, F, TestBounds<M2>, ActorCell<M2, TestBounds<M2>>>
    where
        F: FnOnce(ActorCell<M2, TestBounds<M2>>),
    ;

And you can encapsulate the ability to call run in another trait:

pub trait RunTask {
    fn run_task(self);
}

impl<M, F, CABT, CAB> RunTask for ActorTask<M, F, CABT, CAB>
where
    F: FnOnce(ActorCell<M, CABT>),
{
    fn run_task(self) {
        self.run()
    }
}

And then make sure the output type of spawn meets that bound:

pub trait ActorBounds<M> {
    // ...
    type SpawnOut<M2, F>: RunTask
    where
        F: FnOnce(Self::ChildActorBounds<M2>);

    fn spawn<M2, F>(&self, f: F) -> Self::SpawnOut<M2, F>
    where
        F: FnOnce(Self::ChildActorBounds<M2>);

(And then change your task.run() calls to task.run_task().)[1]

That's enough for your playground, anyway. In a generic context, you no longer know that you're getting an ActionTask out, similar to how you can't be sure ChildActorBounds<_> is always an ActorCell<_, _>. I don't know if this matters to you or not.


A note on how I got there: I started with your playground and then simplified it to the outermost error...

async fn takes_cell<AB>(mut cellu64: AB)
where
    AB: ActorBounds<u64>,
{
    let task = cellu64.spawn::<u32, _>(|cellu32| {
        /*
        let task = cellu32.spawn::<u16, _>(|cellu16| {
            let task = cellu16.spawn::<u8, _>(|cellu8| {
                //
            });
            task.run();
        });
        task.run();
        */
    });
    task.run();
}

Getting rid of type ChildActorBounds<M2>: ActorBounds<M2> allows this to compile, but then the recursive aspect fails when the closure body is uncommented (the dead-end I mentioned at the top).

I started again from this place a second time, and instead attacked the ability to call run (run_task) directly as per the explanation above; this time it worked for the outer problem and still worked when I uncommented the closure body.


  1. Probably you can get rid of this; I stopped poking at it after I got it running. ↩︎

1 Like

Thank you so much for your thoughtful answer and dedicating your time to me. Where can I send you a tip as a sign of appreciation? :grinning:

I have some observations:


It seems like this...
...should go away, since it's always...

That was my thought, too.

...but if you try to require that type to satisfy the ActorBounds<_> bound, recursive bounds on the GAT defeats the compiler, so I guess that's why you have the GAT.

Are you referring to the following situation?

pub trait ActorBounds<M> {
    type ChildActorBoundsType<M2>
    where
        ActorCell<M2, Self::ChildActorBoundsType<M2>>: ActorBounds<M2>;
    // ...
}
overflow evaluating the requirement `<ActorCell<M, StandardBounds<M>> as ActorBounds<M>>::ChildActorBoundsType<M2> == <ActorCell<M, StandardBounds<M>> as ActorBounds<M>>::ChildActorBoundsType<M2>`

overflow evaluating the requirement `<ActorCell<M, TestBounds<M>> as ActorBounds<M>>::ChildActorBoundsType<M2> == <ActorCell<M, TestBounds<M>> as ActorBounds<M>>::ChildActorBoundsType<M2>`

In a generic context, you no longer know that you're getting an ActorTask out, similar to how you can't be sure ChildActorBounds<_> is always an ActorCell<_, _>. I don't know if this matters to you or not.

I would have preferred if the method returned an ActorTask, as I wanted to access the m: PhantomData<M> field in it.
Nevertheless, it is a minor inconvenience, as I can add a getter method in the RunTask trait.


(And then change your task.run() calls to task.run_task().)
Probably you can get rid of this; I stopped poking at it after I got it running.

Now I am really intrigued: what do you mean? Maybe you have something in mind that can improve the user experience; your suggestion would be most welcomed.

I don't have anything set up for that, but I appreciate the sentiment :slight_smile:.

Yes, exactly.

I thought of something else you could do. Now that I've written it out, basically this feels like a hack to get around the lack of higher-ranked equality bounds or the like...

where for<M2> 
    <AB as ActorBounds<u64>>::ChildActorBounds<M2>
    = 
    ActorTask<M2, F, AB::ChildActorBoundsType<M2>, ActorCell<M2, AB::ChildActorBoundsType<M2>>

...or higher-kinded types or something.

But anyway here's the idea:
use std::borrow as brw;
pub trait Is: Into<Self::A> + brw::Borrow<Self::A> + brw::BorrowMut<Self::A> {
    type A;
    // Avoid ambiguity with `RefCell::borrow` and similar
    fn brw(&self) -> &Self::A {
        <Self as brw::Borrow<Self::A>>::borrow(self)
    }
    fn brw_mut(&mut self) -> &mut Self::A {
        <Self as brw::BorrowMut<Self::A>>::borrow_mut(self)
    }
}
impl<A> Is for A {
    type A = A;
}
pub trait ActorBounds<M> {
    type ChildActorBoundsType<M2>;
    type ChildActorBounds<M2>: ActorBounds<M2>
        + Is<A = ActorCell<M2, Self::ChildActorBoundsType<M2>>>;
    type SpawnOut<M2, F>: RunTask
        + Is<A = ActorTask<M2, F, Self::ChildActorBoundsType<M2>, ActorCell<M2, Self::ChildActorBoundsType<M2>>>>
    where
        F: FnOnce(Self::ChildActorBounds<M2>),
    ;
    let task = cellu64.spawn::<u32, _>(|cellu32| {
        // ...
    });

    let _ = &task.brw().m;
    task.run();

The Into parts don't really help as much as I hoped they would, because the implementor is still free to choose whatever type constructor (StandardBounds<_>, ...) they want ChildActorBoundsType<_>, including one where ActorCell<..>: ActorCell<_> doesn't hold. I.e. the required bound isn't where it needs to be.

I attempted a subtrait approach to enforce the bound, but the compiler still fails to see that the bound holds after the call to into.[1] Maybe there's still a way, but I'll leave it here.

It does give you all the getters (via Borrow), so maybe it's still useful.

I just meant you could get rid of the inherent method and just use the trait.

pub trait RunTask {
    fn run(self);
}

impl<M, F, CABT, CAB> RunTask for ActorTask<M, F, CABT, CAB>
where
    F: FnOnce(ActorCell<M, CABT>),
{
    fn run(self) {
        let f = self.f;
        let cell = self.cell;
        &cell.m; // must be able to access m field
        f(cell);
    }
}

Now I better go ahead and hit Reply until your next comment comes in :laughing:.


  1. Messy playground; insert into various places in takes_cell to see failures. ↩︎

1 Like

Hi quine, thank you so much for the reply, I'll get back to it on Monday (bedtime now). :slight_smile:
I was writing the following reply in the exact moment you were typing yours.

It would be equally convenient if run_task was a function instead of a method.
The current approach with the method requires the introduction of the additional RunTask trait.
I was wondering if the approach with a function would result in some simplifications, or if similar issues would still arise.

fn run_task<...>(task: Task<...>) {
    let f = self.f;
    let cell = self.cell;
    &cell.m; // must be able to access m field
    f(cell);
}

async fn takes_cell<AB>(mut cellu64: AB)
where
    AB: ActorBounds<u64>,
{
    let task = cellu64.spawn::<u32, _>(|cellu32| {
        let task = cellu32.spawn::<u16, _>(|cellu16| {
            let task = cellu16.spawn::<u8, _>(|cellu8| {
                //
            });
            run_task(task);
        });
        run_task(task);
    });
    run_task(task);
}

My intuition (did not try yet, bedtime) is that this would probably require adding a GAT in ActorBounds forcing the existence of a function that takes the ActorTask. The bound on the GAT would probably be an FnOnce. Function pointers (fn) implement FnOnce; thus, the structs implementing ActorBounds could specify an fn for the GAT.

Thank you again so much, I am grateful to learn from you.

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.