Design help: Converting generics code to dyn code

āš“ Rust    šŸ“… 2026-02-28    šŸ‘¤ surdeus    šŸ‘ļø 1      

surdeus

Hi!

I would really appreciate code review on Rust Playground

Context

I’m working on a game-theory engine with two core traits:

  • StateEnvironment
  • AI

Multiple AIs compete within a single environment.

Each environment defines associated State and Action types. These must implement:

Eq + Hash + Clone + Debug

The AI algorithms need to store and compare states (e.g., search trees), so those bounds are required.

Previous design

pub trait StateEnvironment {
    type StateT: Eq + PartialEq + Hash + Clone + Debug + 'static;
    type ActionT: Eq + PartialEq + Hash + Clone + Debug + 'static;

    fn initial(&self) -> Self::StateT;
    fn next(&self, state: &Self::StateT, action: &Self::ActionT) -> Self::StateT;
    fn status(&self, state: &Self::StateT) -> GameStatus;
}

pub trait AI<E: StateEnvironment> {
    fn next(&mut self, state: &E::StateT, env: &E) -> E::ActionT;
}

Battle loop:

pub fn battle<'a, E: StateEnvironment>(
    e: &'a E,
    mut actors: Vec<Box<dyn Algorithm<TrackingWrapper<'a, E>>>>,
) -> Result<EndState, BattleError> {
    let mut track_env = TrackingWrapper::new(e);
    
   while let GameStatus::Playing(player_idx) = track_env.status(&state) {
        let player = &mut players[player_idx as usize];
        let action = player.next(&track_env, &state);
        state = track_env.next(&state, &action);
    }
   ....
}

The problem

The environment is often wrapped (tracking, logging, caching, sub-arena selection, etc.).

Because AI is generic over the entire StateEnvironment, any wrapper changes the environment type. That means:

  • The code constructing the AI must know the final wrapped type.
  • But the wrapper stack is often assembled inside battle().
  • This tightly couples AI construction with environment wrapping.
  • Wrappers may also change the State and Action type, so even parametrize AI by those only wouldn't really solve it

The core issue is that the AI and E types must agree on State and Action types and that's a big ol hassle, in the face of wrappers and composition. And really the AI's dont care about the types, they just need the traits.

New attempt

So I decided to completely switch over to dynamic dispatch. This way, the AI doesn't care about the specific type, it just gets something that implements the required traits. So my new main loop would look like this.

fn battle_new<'a>(
    algo1: &'a mut dyn AI,
    algo2: &'a mut dyn AI,
    env: &mut dyn GameEnv,
) {
    let mut state = env.initial();

    while let GameStatus::Playing(player_idx) = env.status(&state) {
        let player = &mut players[player_idx as usize];
        let action = player.next(env, &state);
        state = env.next(&state, &action);
    }

    println!("Result: {:?}", env.status(&state));
}

However in the effort to make this work, I feel I have created a monster

#[derive(Clone)]
pub struct ObjectRef(pub Rc<dyn ObjectAPI>);

pub trait ObjectAPI {
    fn eq_dyn(&self, other: &dyn Any) -> bool;
    fn hash_dyn(&self, state: &mut dyn Hasher);
    fn as_any(&self) -> &dyn Any;
    fn debug_dyn(&self, f: &mut Formatter<'_>) -> fmt::Result;
}

So now the AI trait can be

pub trait AI {
    fn next(&mut self, environment: &dyn GameEnv, state: &ObjectRef) -> ObjectRef;
}

Now all states and actions in my program would be wrapped in ObjectRef, killing performance, introducing a lot of boilerplate and overall forcing Rust to be more like JavaScript.

Is this how dyn is supposed to be done?

I understand that I basically welded a dynamic typing system on Rust, and wiped all of type safety with it, but... is there any way to do what I want, without sacrificing type safety? I especially don't like that Env::next() takes in an ObjectRef (wrapped State) and returns and ObjectRef (also wrapped State). I could trivially accidentally return an Action instead of a State, and the type system would be perfectly happy.

What I tried but didn't work:

battle() wouldn't get the final AI types but would get &dyn AIFactory with make<T>() -> Box<&dyn AI>

  • Failed: dyn types can't have type parameters

Skipping the ObjectRef type

  • Failed: I need the action and state types to implement Hash, Clone, Debug, Eq, and I cant do blanket impl on Rc<dyn>

5 posts - 2 participants

Read full topic

šŸ·ļø Rust_feed