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
)?
Yes! There are a number of ways of solving this problem
and
KeywordI 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.
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 }
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!