0

I'm trying to work out some design issues on code for a rather basic reinforcement learning library I'm working on, but can't seem to figure out where the flaw is. There's this base code which relies on generics:

public abstract class Action {...}

public abstract class State<TAction>
    where TAction : Action
{
    ...

    public abstract TAction[] GetAllowedActions();
    
    public virtual Pair<TAction, State<TAction>> GetPair(TAction action)
    {
        return new Pair<TAction, State<TAction>>(this, action);
    }
}

public class Pair<TAction, TState>
    where TAction : Action
    where TState : State<TAction>
{
    public Pair(TState state, TAction action) {...}

    public virtual IEnumerable EquivalentPairs()
    {
        yield return this;
    }
}

Now I want to build on given code by extending the classes for a certain game I want to train my agent on:

public class GameAction : Action {...}

public class GameState : State<GameAction>
{
    public override Pair<GameAction, State<GameAction>> GetPair(GameAction action)
    {
        return new GamePair(this, action); // CS0029 - GamePair cannot be converted to Pair<GameAction, GameState> 
    }
}

public class GamePair : Pair<GameAction, GameState> // implements additionally required behavior
{
    public GamePair(GameState state, GameAction action) : base(state, action) {...}
}

While Pair wouldn't necessarily have to be extended, it does in this case where certain states being equivalent want to be accounted for, with their corresponding actions adapted accordingly - e.g. in Connect Four, where you additionally could mirror the current game state and the according column being picked to improve training. That's why I'd like additional behavior on this class to be allowed.

This brings me across the problem though, that I can't generally return GamePair with GetPair(...), since it's not recognized as a viable subclass of Pair<GameAction, State<GameAction>>, although GameState is a subclass of State<GameAction>. How to deal with this?

StrikeAgainst
  • 556
  • 6
  • 23
  • 2
    You would need to turn you abstract classes into interfaces, and use generic covariance. Because otherwise although `GamePair` is a `Pair`, it is not also a `Pair>`. Consider if it would be allowed to: a function would call `GetPair()` expecting a `Pair>` it would then try to use it with a different object that is not a `GamePair` but is a `State` and would fail or run incorrectly. – Charlieface Jan 02 '22 at 06:03
  • Interesting, I haven't heard about the concept of generic variance before (still quite new to C#). This did indeed fix the issue, thank you a lot. – StrikeAgainst Jan 02 '22 at 14:07
  • Yeah, it got a lot clearer now. I'm running into new issues with this now though: Due to the covariance, the method signature `IPair> GetPair(TAction action);` is no longer allowed for the `IState` interface, so I'd have to resort to `IAction` for the parameter. When implementing this method in a state class though, `IAction` is usually too loose of a type. I can't quite see the misconception here. – StrikeAgainst Jan 02 '22 at 16:04
  • You could make it `TPair GetPair(TAction action) where TPair : IPair>` You should have an option above of accepting this duplicate link and closing the question – Charlieface Jan 02 '22 at 16:07
  • Like, giving the `GetPair` method generic types as well? That would mean that everywhere I call it - e.g. any algorithm class - I'd have to induce generic types for `IState` and `IAction` as well, which I'd like to avoid, as this would generate a lot of overhead in my code. Apart from that, VSC registers errors on that signature of yours... not sure if covariance is supposed to work on method signatures. – StrikeAgainst Jan 02 '22 at 16:31
  • Sorry you're right, you'd need to move `` to the interface declaration for this to work – Charlieface Jan 02 '22 at 16:32
  • That's not possible either, since I'd get a circular dependency with my generics via `TPair : IPair>`. Making `TAction` contravariant won't work either since I have some properties having the type `TAction`. – StrikeAgainst Jan 03 '22 at 17:02

0 Answers0