Haskell - 解决循环模块依赖问题

14

假设我写了以下代码:

一个游戏模块

module Game where 
import Player
import Card 
data Game = Game {p1 :: Player,
                  p2 :: Player,
                  isP1sTurn :: Bool
                  turnsLeft :: Int
                 }

一个播放器模块

module Player where
import Card
data Player = Player {score :: Int,
                      hand :: [Card],
                      deck :: [Card]
                     }

和一个卡片模块

module Card where
data Card = Card {name :: String, scoreValue :: Int}

然后我写了一些代码来实现逻辑,使玩家轮流从手牌中抽取和打出卡牌,以增加得分,直到游戏结束。

然而,完成这段代码后,我意识到我编写的游戏模块很无聊!

我想要重新设计这个纸牌游戏,使打出一张卡牌不仅仅是增加得分,而是任意改变游戏规则。

于是,我将Card模块更改为以下内容:

module Card where
import Game
data Card = Card {name :: String, 
                  onPlayFunction :: (Game -> Game)            
                  scoreValue :: Int}

当然,这使得模块导入形成了一个循环。

我该如何解决这个问题?

简单的解决方案:

将所有文件移动到同一个模块中。这可以很好地解决问题,但会降低模块化程度;我以后无法在另一个游戏中重用相同的卡牌模块。

维护模块的解决方案:

Card添加一个类型参数:

module Card where
data Card a = {name :: String, onPlayFunc :: (a -> a), scoreValue :: Int}

Player添加另一个参数:

module Player where
data Player a {score :: Int, hand :: [card a], deck :: [card a]}

对于Game,进行最后一次修改:

module Game where
data Game = Game {p1 :: Player Game,
                  p2 :: Player Game,
                 }

这保持了模块化,但需要我向我的数据类型添加参数。如果数据结构更深嵌套,我可能不得不向我的数据添加 - 很多 - 参数,如果我必须为多个解决方案使用此方法,我可能会得到数量繁杂的类型修饰符。

那么,有没有其他有用的解决方案来解决此重构问题,或者这些是唯一的两个选项?

1个回答

10

你的解决方案(添加类型参数)并不差。你的类型更通用了(如果需要,可以使用Card OtherGame),但如果你不喜欢额外的参数,则可以选择以下两种方式:

  • 编写一个模块CardGame,其中仅包含互相递归的数据类型,并在其他模块中导入该模块,或者
  • ghc中使用{-# SOURCE #-} pragma来打破循环依赖关系

最后一种解决方案需要编写一个Card.hs-boot文件,其中包含Card.hs中类型声明的子集。


4
除非真的必要,否则我强烈建议避免使用 {-# SOURCE #-} / .hs-boot 机制。 - leftaroundabout
1
@leftroundabout:是的,我觉得这个有点繁琐和不舒服,但除了在wiki中提到的那些论点之外,还有其他反对的理由吗?在我看来,这些理由对于小项目来说并不那么相关。 - Hans Lub

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接