Let's say I write the following code:
a game module
module Game where
import Player
import Card
data Game = Game {p1 :: Player,
p2 :: Player,
isP1sTurn :: Bool
turnsLeft :: Int
}
a player module
module Player where
import Card
data Player = Player {score :: Int,
hand :: [Card],
deck :: [Card]
}
and a card module
module Card where
data Card = Card {name :: String, scoreValue :: Int}
I then write some code to implement logic where players take turns drawing and playing cards from their hand to add bonuses to their score's until the game runs out of turns.
However, I realize upon completion of this code that the game module I've written is boring!
I want to refactor the card game so when you play a card, rather than just adding a score, instead the card arbitrarily transforms the game.
So, I change the Card
module to the following
module Card where
import Game
data Card = Card {name :: String,
onPlayFunction :: (Game -> Game)
scoreValue :: Int}
which of course makes the module imports form a cycle.
How do I resolve this problem?
Trivial Solution:
Move all the files to the same module. This solves the problem nicely, but reduces modularity; I can't later reuse the same card module for another game.
Module maintaining solution:
Add a type parameter to Card
:
module Card where
data Card a = {name :: String, onPlayFunc :: (a -> a), scoreValue :: Int}
Add another parameter to Player
:
module Player where
data Player a {score :: Int, hand :: [card a], deck :: [card a]}
With one final modification to Game
:
module Game where
data Game = Game {p1 :: Player Game,
p2 :: Player Game,
}
This keeps modularity, but requires me to add parameters to my data types. If the data structures were any more deeply nested I could have to add -a lot- of parameters to my data, and if I had to use this method for multiple solutions, I could end up with an unwieldy number of type modifiers.
So, are there any other useful solutions to resolving this refactor, or are these the only two options?
Your solution (adding type parameters) is not a bad one. Your types become more general (you could use Card OtherGame
if you need it), but if you dislike the extra parameters you could either:
CardGame
that contains (just) your mutually recursive datatypes, and import this module in the other ones, orghc
, use {-# SOURCE #-}
pragmas to break the circular dependency
This last solution requires the writing of a Card.hs-boot
file with a subset of the type declarations in Card.hs
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With