Search code examples
f#circular-dependency

F# circular type dependencies


I'm creating a little card game using F# but I'm having a few problems due to circular type dependencies. I have the following card types (simplified):

type Monster =
    { Health: int
      Attack: int
      SkillText: string option }

type Spell =
    { EffectText: string }

type Kind =
    | Monster of Monster
    | Spell of Spell

type Card =
    { Name: string
      Image: string
      Kind: Kind
      IsPlayer: bool
      CanPlay: Board -> bool }

The idea is that, when creating a new card, it must define the conditions needed for it to be played based on the current board state.

The board type is

and Board =
    { PlayerHand: Hand
      EnemyHand: Hand
      PlayerDeck: Deck
      EnemyDeck: Deck
      PlayerField: Field
      EnemyField: Field
      PlayerGraveyard: Graveyard
      EnemyGraveyard: Graveyard }

My problem is, the types inside the Board all depend on the Card type, so I need to define it after the Card type, but the Card type depends on the Board type. I know that I could use generics to undo the cyclic dependency, but I have the following problem with it:

If I define a Board<'Card> type I would need to make all the types referenced by Board as generic. That not only seems verbose to me, but it also doesn't make a lot of sense in my application. I'll never make a hand of anything that isn't a Card. I know that I could do Card<'Board> instead, but that seems less intuitive and the same problems I have with Board<'Card> apply.

Is there any way to solve this problem without generics while staying purely functional (other than using the and keyword for every type that depends on Card)?


Solution

  • Yes! There are a number of ways of solving this problem

    1. The and Keyword

    I see you already used it in and Board = ... so I assume you know are aware of it but you don't want to use it. Just In case:

    type Card =
        { Name: string
          Image: string
          Kind: Kind
          IsPlayer: bool
          CanPlay: Board -> bool }
    
    and Board =
        { PlayerHand: Hand
          EnemyHand: Hand
          PlayerDeck: Deck
          EnemyDeck: Deck
          PlayerField: Field
          EnemyField: Field
          PlayerGraveyard: Graveyard
          EnemyGraveyard: Graveyard }
    

    This lets you write circular types together, but they must be in the same file.

    2. Break Down Your Dependency

    This depends on the game you have in mind, but maybe CanPlay does not need to depend on board? Usually things like this can be a simple enum:

    type PlayCondition =
        | BoardIsEmpty
        | ManaIsAbove of int
    
    type Card =
        { Name: string
          Image: string
          Kind: Kind
          IsPlayer: bool
          CanPlay: PlayCondition }
    

    3. Use an Interface

    Define your interface with the functions you want to use on the board:

    type IBoard =
        abstract member GetCardAt: x: int -> y: int -> Card
    
    and Card =
        { Name: string
          Image: string
          Kind: Kind
          IsPlayer: bool
          CanPlay: IBoard -> bool }
    

    Implement it on board:

    type Board =
        { PlayerHand: Hand
          EnemyHand: Hand
          PlayerDeck: Deck
          EnemyDeck: Deck
          PlayerField: Field
          EnemyField: Field
          PlayerGraveyard: Graveyard
          EnemyGraveyard: Graveyard }
        interface IBoard with
            member this.GetCardAt x y = ...
    

    Hope this helps!