Haskell - resolving cyclical module dependency
Asked Answered
F

1

14

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?

Flatfish answered 2/5, 2016 at 9:7 Comment(0)
Z
10

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:

  • write a module CardGame that contains (just) your mutually recursive datatypes, and import this module in the other ones, or
  • in ghc, 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.

Zooplasty answered 2/5, 2016 at 9:30 Comment(3)
I would rather heavily recommend avoiding the {-# SOURCE #-} / .hs-boot mechanism, unless it's really necessary.Albaugh
@leftroundabout: Yes, I find it fiddly and uncomfortable, but are there any arguments against it othter than the ones mentioned in the wiki, which are (imho) not so relevant for small projects?Zooplasty
I've successfully used {-# SOURCE #-} to break up a big parser source file (based on Text.Megaparsec). Formal grammars tend to be notoriously self-referential and the main alternative (dependency injection) is rather messy in Haskell, so I think that's a reasonable use case.Geoff

© 2022 - 2024 — McMap. All rights reserved.