Collection of trait implementations with associated types (GATs?)

I have a trait with one function:

type Arg = (); // Not relevant for this example.
type Res = (); // Not relevant for this example.

trait Call {
    fn call(&self, arg: Arg) -> Option<Res>;
}

Now I want to implement this trait for different structs.

Some structs may not want to handle specific Arg and so they return None.

Instead of having return type of Option<Res> I would like to enforce this rule in compile time, with something like this:

trait Call {
    type Token;

    fn can_call(&self, arg: &Arg) -> Option<Token>;

    fn call(&self, token: Self::Token, arg: Arg) -> Res;
}

It doesn't have to be token-based approach, but it seems reasonable.

The problem is when I want to build a pipeline of implementations.

Obviously, I can't have

struct Callables {
    callables: Vec<Box<dyn Call>>,
}

since Call needs to know the type of associated Token.

I suppose there is a way to do this via layering Call implementations in each other:

struct Stack<A, AToken, B, BToken> {
    callable: A<Token = AToken>,
    next: B<Token = BToken>,
}

But I don't exactly know how to build such Stack-s and then store them.

Can anyone tell me if this is even possible?

trait Call {
    fn can_call(&self, arg: Arg) -> Option<fn(&Self, arg: Arg) -> Res>;
}

Though I don't really see the advantage of doing this.

Also, what's stopping someone from passing different Arg values to can_call and call? If you don't trust code to not get a token from implementor A and pass it to implementor B [1], why trust them to not change up the Arg on you?

To address that, you could instead have one of

    // Intention: Capture arg in closure.  Maybe need `FnOnce`
    fn can_call(&self, arg: Arg) -> Option<Box<dyn FnMut(&Self) -> Res>>;
    // Intention: Capture arg and &self in closure.  Maybe need `FnOnce`
    fn can_call(&self, arg: Arg) -> Option<Box<dyn FnMut() -> Res + '_>>;

All of these lose the ability to use method call syntax.


Taking a step back, maybe you want an enum instead of a dyn Call. Then the type-based information of what variant payloads have which methods is available at compile time.


  1. I assume that's why Token was an associated type anyway ↩ī¸Ž

3 Likes

why trust them to not change up the Arg on you?

That's a valid point.

Let me rephrase the problem: for the following trait

trait Call {
    type Token;

    fn can_call(&self) -> Option<Self::Token>;

    fn call(&self, token: Self::Token);
}

how can I combine multiple implementations as described in the OP?

Unless the implementations share the same associated type, you can't. Rust is strongly typed. So here:

fn foo(dc: Box<dyn Call>) {
    let token = dc.can_call().unwrap();
}

token has to have a single, statically known type.


I just realized my first two suggestions probably don't actually work for dyn Call either, since they use Self. One work around would be to use &dyn Call and downcast or such, but that would put you right back at "have to trust the caller to orchestrate calls together sensibly". Or in other words, when you're type erased (Box<dyn Call>), the caller can switch up &self on you too.

Capturing &self would still work though.

1 Like

I know it is :slight_smile:

I was wondering if I'm missing something that would allow me to do the thing I wanted to do.

This question is somewhat inspired by this PR which is trying to kinda do the same thing.

The only other thing I thought of was panicking if they hold it wrong. These all also assume that once you get permission (a token or a FnOnce or whatever), that permission is good "forever" (or as long as your shared borrow in the case of a &self-capturing closure).

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.