Since dyn don't like GATs at all, how can I use generics to handle this case with multiple mods Trait?

Given:

pub struct Needed {
    pub repository_commands: Arc<dyn Trait>,
    // and similar others...
}

and Trait:

pub trait Trait: Send + Sync + Player + Shirt {}

impl<T: Player + Shirt> Trait for T {}

#[async_trait::async_trait]
pub trait PlayerCreateTrait {
    //...
}

#[async_trait::async_trait]
pub trait Player: Send + Sync {
    type PlayerCreate<'input>: Send + PlayerCreateTrait;

    async fn player_create_start<'input>(
        &self,
        input: &'input PlayerInput,
    ) -> Result<Self::PlayerCreate<'input>, String>;

    //...
}

#[async_trait::async_trait]
pub trait Shirt: Send + Sync {
    //...
}

Right now I have 200+ Handler structs all equals, one for each command of my application and all of them are like this:

pub struct Handler {
    needs: Arc<Needed>,
}

Each of them has a different handle() method (based on which command is). And each of them is in a separate file (mod).

This is for example for create_player:

pub struct Handler {
    needs: Arc<Needed>,
}

impl Handler {
    pub async fn handle(&self, input: &PlayerCreateInput) -> Result<DomainPlayerCreated> {
        // create_player
    }
}

This is for update_player:

pub struct Handler {
    needs: Arc<Needed>,
}

impl Handler {
    pub async fn handle(&self, input: &PlayerUpdateInput) -> Result<DomainPlayerUpdated> {
        // update_player
    }
}

This is for team_engage:

pub struct Handler {
    needs: Arc<Needed>,
}

impl Handler {
    pub async fn handle(&self, team_id: String, player_ids: Vec<String>) -> Result<DomainTeamEngaged> {
        // team_engage
    }
}

and so on...

I asked for help (for another issue) and a great and kind person posted me an amazing example.

But in this example there are many news for me, one of them are the associated types.

And I just found out that GATs (generics associated types) are not liked by dyn at all (Rust Explorer):

error[E0191]: the value of the associated type `PlayerCreate` (from trait `Player`) must be specified
   --> src/main.rs:160:42
    |
142 |         type PlayerCreate<'input>: Send + PlayerCreateTrait;
    |         --------------------------------------------------- `PlayerCreate` defined here
...
160 |         pub repository_commands: Arc<dyn Trait>,
    |                                          ^^^^^ help: specify the associated type: `Trait<PlayerCreate = Type>`

So the great user suggested me to use generics to work-around this problem, going from:

// in this example `Needed` struct is not used

pub struct Handler {
    pub repository_commands: Arc<dyn Trait>,
}

impl Handler {
    pub fn new(repository_commands: Arc<dyn Trait>) -> Self {
        Self {
            repository_commands,
        }
    }

    pub async fn handle(&self, input: &PlayerInput) -> Result<DomainPlayer, String> {
        // ...
    }
}

to this (you can interact with the code here: Rust Explorer)

struct HandlerInner<C> {
    repository_commands: C,
}

#[async_trait::async_trait]
trait ErasedHandler {
    async fn handle(&self, input: &PlayerInput) -> Result<DomainPlayer, String>;
}

#[async_trait::async_trait]
impl<C: Send + Sync + Trait> ErasedHandler for HandlerInner<C> {
    async fn handle(&self, input: &PlayerInput) -> Result<DomainPlayer, String> {
        // ...
    }
}

pub struct Handler(Arc<dyn ErasedHandler>);

impl Handler {
    pub fn new<C: 'static + Trait>(repository_commands: C) -> Self {
        Self(Arc::new(HandlerInner {
            repository_commands,
        }))
    }
    pub async fn handle(&self, input: &PlayerInput) -> Result<DomainPlayer, String> {
        self.0.handle(input).await
    }
}

but I cannot understand how to do this in my case: the complete and interactive code is on this playground here.

As you can see there are many mods there:

pub mod command {
    pub mod player {
        //...
    }
    pub mod shirt {
        //...
    }
    pub mod team {
        //...
    }
}

is there a "simpler" way to avoid ErasedHandler and all that?

Since I have 200+ equals handlers (one for each command) altough with different handle method, maybe using generics is a great solution, but how in this case?

I would suggest starting with a high level description of what you want to do. This direction doesn't make sense to me:

trait ErasedHandler {
    async fn handle(&self, input: &PlayerInput) -> Result<DomainPlayer, String>;
}

because not all of your handle methods take a &PlayerInput and return a DomainPlayer.

The simplest solution for the example as given: each Handler is identical and has the same method name with signatures. You don't use those methods in a generic way. Why not simply write

pub struct Handler {
  // ...
}
impl Handler
{
  pub async fn handle_create_player(&self, input: &PlayerCreateInput) -> Result<DomainPlayerCreated>
  pub async fn handle_update_player(&self, input: &PlayerUpdateInput) -> Result<DomainPlayerUpdated> 
  pub async fn handle_team_engage(&self, team_id: String, player_ids: Vec<String>) -> Result<DomainTeamEngaged>
}

btw, if you are okay with simply naming the specific thing you're handling every time you handle it, you don't need to jump directly to a generic GAT-based solution. Just write what I've written above. handle_create_player is the same amount of type as create_player::handle. It's not clear if you really need generics at all (you never showed how you want to use these in a generic manner)

But if the goal here is specifically to make these generic? Then, rather than writing hundreds of nearly identical impls, and trying to guess at a common interface these might belong to, start by designing the interface, then writing the impls. If you understand the purpose of the interface, and how you will use it, it will be easier to see how to encode it as a Trait.

Finally: the error you get in your example is not caused by GATs specifically, it would occur with any associated type. But this error is a red herring, the issue is in the design of the program, and not having a clear understanding of why exactly GATs will help.

2 Likes

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.