#Generics vs. `dyn impl`

26 messages · Page 1 of 1 (latest)

proven marsh
#

I'm trying to figure out how to handle simulating many "games" (in the game theory sense of the word) with various strategies. The end goal is to be able to know that for a given game, this choice of 10 strategies will work the best. I'm getting stuck at how to handle getting the different strategies to play the game.

That probably doesn't make a lot of sense, so here's the code I'm currently working with:

trait Strategy {
    fn play_game(self: Self, current_game: Game, last_results: Option<GameResult>) -> bool;
}

struct Game {
    a: i32,
    b: i32,
    c: i32,
    d: i32,
    
    strategy_a: Box<dyn Strategy>,
    strategy_b: Box<dyn Strategy>,
}
    
struct GameResult {
    game: Game,
    
    a_choice: bool,
    b_choice: bool,
    
    a_score: i32,
    b_score: i32,
}

impl GameResult {
    fn opponent_chose(self: Self, player: &dyn Strategy) -> bool {
        todo!()
    }
}

struct StrategyAlwaysA {}

impl Strategy for StrategyAlwaysA {
    fn play_game(self: Self, _current_game: Game, _last_results: Option<GameResult>) -> bool { false }
}

struct StrategyAlwaysB {}

impl Strategy for StrategyAlwaysB {
    fn play_game(self: Self, _current_game: Game, _last_results: Option<GameResult>) -> bool { true }
}

struct StrategyTitForTat {}

impl Strategy for StrategyTitForTat {
    fn play_game(self: Self, current_game: Game, last_results: Option<GameResult>) -> bool { 
        last_results.map(|r| r.opponent_chose(&self)).unwrap_or(false)
    }
}

A major component of this is being able to generate thousands of games, and then doing statistics on the games where (for example) strategyAlwaysA played, and figuring out what values for the Game said strategy does well with over other strategies. Does that mean I shouldn't use just a struct/trait object for the strategy choice in each game? Does the fact that I'm generating games with all sorts of strategies mean generics are a bad fit? How far off am I from idiomatic Rust code so far?

lilac pollen
proven marsh
#

I'm having a hard time finding more info on the "trait objects are bad" bit. Any suggestions for how to google that, or a source?

In order to have a list of Games, though, doesn't that mean I can't use Generics? So enum it would be, then, and then I'd have to do matching in code in order to manually dispatch the right function for the strategy chosen in the match?

lilac pollen
#

generally if you have a case that an object can be one of 10 different options, its just faster, easier to refactor, more expressive and generally easier to work with if you use an enum, compared to a trait object

#

I dont really know any resource for this, and it would take examples and a lot of discussion to go through the reasons

proven marsh
#

Fair enough.

lilac pollen
#

and if you find that you need to include some data inside the enum for some strategy to work, thats easy. just add data to the enum variant that represents the strategy

proven marsh
#

Yeah, the hangup in my head is having lots of unrelated code amalgamated in a single method. If rust had impl for specific enum variants, that'd make this easier, but I'm guessing a single function with a match in it is probably the more idiomatic choice?

lilac pollen
#

basically you can replace impl on specific variants by making a standalone function that represents the variant.
another option is to make a struct StrategyOne, where the Strategy enum would have a variant StrategyOne(StrategyOne),. then you can implement the method you want on the struct contained in that variant (I only recommend this if the strategy already has data associated with it, which would go in the struct)

proven marsh
#

I think there should be a strategy or two that actually have state, but I need to refresh my memory on that. It's possible all of the strategies can be done without state given the function signature I have right now.

#

But yeah, those three strategies just all happened to not need state.

#

...Nope, only one of them needs state, and the only state that needs to be kept is a handle to an RNG. That's it.

lilac pollen
#
enum Strategy {
    StrategyOne,
    StrategyTwo(RngHandle),
    StrategyThree,
}

impl Strategy {
    fn apply(&mut self, state: &mut GameState) {
        match self {
            StrategyOne => strategy_one(state),
            StrategyTwo(rng) => strategy_two(state, rng),
            StrategyThree => strategy_three(state),
        }
    }
}

fn strategy_one(state: &mut GameState) {
    ...
}

fn strategy_two(state: &mut GameState, &mut RngHandle) {
    ...
}

fn strategy_three(state: &mut GameState) {
    ...
}```
#

rough draft of what the code of applying different strategies to the game would look like

#

I added the RNG handle thing to that draft

#

enums are just very flexible. Youre not writing any kind of contracts to some common interface that the strategies have, you just write the straight logic in a data oriented way that you can easily refactor

proven marsh
#

Yeah, I've seen rust enums a few times before. My brain's just stuck in a more OOP-ish world.

lilac pollen
#

data oriented means you act on the actual data directly, instead of layers of abstraction in between

#

common abstract interfaces with dynamic types seem nice, but its easy to misunderstand the requirements of what youre writing, and then you have to rewrite it all later

lilac pollen
proven marsh
#

I think I've got a decent picture, let's see what kind of code I can crank out.

#

Thanks for the help, TheRoboMan. 🙂

lilac pollen
# proven marsh Yeah, the hangup in my head is having lots of unrelated code amalgamated in a si...

by the way, to avoid making one huge method, what I suggested was to split each strategy into its own standalone function. One readability problem with that is that there is nothing indicating that the functions are not meant to be called anywhere other than the main .apply method, even though its supposed to just spread out the logic into chunks.
The idiomatic way to get around that would be to put all the logic in a module, where the apply method is public but everything else is private

#

I guess its compareable to OOP encapsulation, but more loosely defined and not tied to the existence of any objects